mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-24 08:32:25 +02:00
247 lines
9.1 KiB
Python
247 lines
9.1 KiB
Python
# coding: utf-8
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import time
|
||
|
||
from .base import BaseCreator
|
||
from ..core.constants import PHP_RUNTIME_VERSIONS, locks_dir
|
||
from ..core.exceptions import ProjectImportError
|
||
from ..core.json_store import file_lock
|
||
|
||
|
||
PHP_INSTALL_TIMEOUT = 900
|
||
PHP_ROOT = "/www/server/php"
|
||
|
||
|
||
def get_installed_php_versions(php_root=None):
|
||
"""Return PHP runtimes offered by the import wizard.
|
||
|
||
Installed versions are discovered from the filesystem under
|
||
``<php_root>/<version>/bin/php``. The bundled default versions are always
|
||
included (``installed=False`` when missing) so the wizard can auto-install
|
||
them later. Optional arguments and environment variables make the
|
||
filesystem scan testable without changing production paths.
|
||
"""
|
||
php_root = php_root or os.environ.get("AAPANEL_PHP_ROOT", PHP_ROOT)
|
||
installed = set()
|
||
if os.path.isdir(php_root):
|
||
for version in os.listdir(php_root):
|
||
if os.path.isfile(os.path.join(php_root, version, "bin", "php")):
|
||
installed.add(version)
|
||
values = set(PHP_RUNTIME_VERSIONS) | installed
|
||
options = [{
|
||
"label": _display_version(version),
|
||
"value": version,
|
||
"installed": version in installed,
|
||
} for version in values]
|
||
options.sort(key=lambda item: _php_version_sort_key(item["value"]), reverse=True)
|
||
return options
|
||
|
||
|
||
def _php_version_sort_key(version):
|
||
numbers = re.findall(r"\d+", str(version))
|
||
return (int(numbers[0]) if numbers else 0,)
|
||
|
||
|
||
def prepare_php_config(config):
|
||
"""Validate and normalize the selected PHP runtime."""
|
||
version = str(config.get("php_version", "")).strip().replace(".", "")
|
||
installed = [
|
||
item["value"]
|
||
for item in get_installed_php_versions()
|
||
if item.get("installed")
|
||
]
|
||
if version not in PHP_RUNTIME_VERSIONS and version not in installed:
|
||
supported = sorted(
|
||
set(PHP_RUNTIME_VERSIONS) | set(installed),
|
||
key=_php_version_sort_key,
|
||
reverse=True,
|
||
)
|
||
raise ProjectImportError(
|
||
"Unsupported PHP runtime '{}'; supported versions: {}".format(
|
||
version, ", ".join(supported),
|
||
),
|
||
"PHP_RUNTIME_UNSUPPORTED",
|
||
)
|
||
config["php_version"] = version
|
||
return config
|
||
|
||
|
||
def prepare_php_runtime(config, progress=None):
|
||
"""Queue installation of the selected PHP runtime and wait until it is usable."""
|
||
import public
|
||
from panel_guide_page import GuidePage
|
||
|
||
prepare_php_config(config)
|
||
version = config["php_version"]
|
||
php_bin = os.path.join(
|
||
public.GetConfigValue("setup_path"),
|
||
"php", version, "bin", "php",
|
||
)
|
||
with file_lock(os.path.join(locks_dir(), "php_runtime.install.lock")):
|
||
_report_runtime(progress, 0.05, "Checking PHP {}".format(_display_version(version)))
|
||
if os.path.isfile(php_bin):
|
||
_report_runtime(progress, 1, "PHP {} is ready".format(_display_version(version)))
|
||
return version
|
||
|
||
deadline = time.time() + PHP_INSTALL_TIMEOUT
|
||
while _active_php_install_count(public):
|
||
if os.path.isfile(php_bin):
|
||
_report_runtime(progress, 1, "PHP {} is ready".format(_display_version(version)))
|
||
return version
|
||
if time.time() >= deadline:
|
||
raise ProjectImportError(
|
||
"Timed out waiting for another PHP installation task",
|
||
"PHP_RUNTIME_INSTALL_TIMEOUT",
|
||
)
|
||
_report_runtime(progress, 0.1, "Waiting for the current PHP installation task")
|
||
time.sleep(2)
|
||
|
||
_report_runtime(progress, 0.15, "Installing PHP {}".format(_display_version(version)))
|
||
if not GuidePage().add_soft_install_task("php", version):
|
||
raise ProjectImportError(
|
||
"Failed to queue PHP {} installation".format(_display_version(version)),
|
||
"PHP_RUNTIME_INSTALL_FAILED",
|
||
)
|
||
|
||
started_at = time.time()
|
||
while time.time() < deadline:
|
||
if os.path.isfile(php_bin):
|
||
_report_runtime(progress, 1, "PHP {} is ready".format(_display_version(version)))
|
||
return version
|
||
if not _active_php_install_count(public):
|
||
break
|
||
elapsed = time.time() - started_at
|
||
ratio = min(0.95, 0.15 + (elapsed / PHP_INSTALL_TIMEOUT) * 0.8)
|
||
_report_runtime(progress, ratio, "Installing PHP {}".format(_display_version(version)))
|
||
time.sleep(2)
|
||
|
||
if time.time() >= deadline:
|
||
raise ProjectImportError(
|
||
"PHP {} installation timed out".format(_display_version(version)),
|
||
"PHP_RUNTIME_INSTALL_TIMEOUT",
|
||
)
|
||
raise ProjectImportError(
|
||
"PHP {} installation completed without a usable PHP binary".format(
|
||
_display_version(version),
|
||
),
|
||
"PHP_RUNTIME_INSTALL_INCOMPLETE",
|
||
)
|
||
|
||
|
||
def _active_php_install_count(public):
|
||
return int(public.M("tasks").where(
|
||
"status!=? and name LIKE ?",
|
||
("1", "%php%"),
|
||
).count() or 0)
|
||
|
||
|
||
def _display_version(version):
|
||
return "{}.{}".format(version[0], version[1:])
|
||
|
||
|
||
def _report_runtime(progress, ratio, message):
|
||
if progress:
|
||
progress(ratio, message)
|
||
|
||
|
||
class PHPCreator(BaseCreator):
|
||
static = False
|
||
|
||
def create(self):
|
||
import public
|
||
from panel_site_v2 import panelSite
|
||
|
||
domains = self.domains()
|
||
if not domains:
|
||
raise ProjectImportError("Domain name is required", "DOMAIN_REQUIRED")
|
||
primary = domains[0]
|
||
domain_list = []
|
||
for domain in domains[1:]:
|
||
domain_list.append(domain if ":" in domain else domain + ":80")
|
||
args = public.to_dict_obj({
|
||
"webname": json.dumps({"domain": primary, "domainlist": domain_list, "count": len(domain_list)}),
|
||
"type": "PHP",
|
||
"ps": str(self.config.get("project_name", primary)),
|
||
"path": self.project_path,
|
||
"version": "00" if self.static else str(self.config.get("php_version", "83")),
|
||
"sql": "",
|
||
"datapassword": "",
|
||
"datauser": "",
|
||
"codeing": "utf8mb4",
|
||
"port": "80",
|
||
"type_id": 0,
|
||
"force_ssl": 0,
|
||
"ftp": False,
|
||
"is_create_default_file": False,
|
||
"ssl_auto": 1 if self.config.get("ssl_enabled") else 0,
|
||
"sub_dir": "",
|
||
"project_type": "PHP",
|
||
})
|
||
result = panelSite().AddSite(args)
|
||
payload = self.ensure_success(result)
|
||
site_id = payload.get("siteId", payload.get("id", 0)) if isinstance(payload, dict) else 0
|
||
warnings = []
|
||
install_warning = self._install_composer_dependencies()
|
||
if install_warning:
|
||
warnings.append(install_warning)
|
||
return {
|
||
"site_id": int(site_id or 0),
|
||
"project_name": primary,
|
||
"domain": primary,
|
||
"project_type": "static" if self.static else "php",
|
||
"path": self.project_path,
|
||
"ssl_requested": bool(self.config.get("ssl_enabled")),
|
||
"warnings": warnings,
|
||
"raw": payload if isinstance(payload, dict) else {},
|
||
}
|
||
|
||
def _install_composer_dependencies(self):
|
||
"""Install composer dependencies after the site is created when requested.
|
||
|
||
A failed install is downgraded to a warning; the site remains usable and
|
||
the user can install the dependencies manually later.
|
||
"""
|
||
if self.static:
|
||
return ""
|
||
if not bool(self.config.get("install_dependencies", True)):
|
||
return ""
|
||
composer_json = os.path.join(self.project_path, "composer.json")
|
||
if not os.path.isfile(composer_json):
|
||
return ""
|
||
if os.path.isdir(os.path.join(self.project_path, "vendor")):
|
||
return ""
|
||
import public
|
||
php_bin = os.path.join(
|
||
public.GetConfigValue("setup_path"),
|
||
"php", str(self.config.get("php_version", "83")), "bin", "php",
|
||
)
|
||
composer_bin = "/usr/bin/composer"
|
||
if not os.path.isfile(php_bin) or not os.path.isfile(composer_bin):
|
||
return "Composer is not available; PHP dependencies were not installed"
|
||
log_path = "/tmp/project_import_composer.log"
|
||
command = (
|
||
"cd {} && export COMPOSER_ALLOW_SUPERUSER=1 && "
|
||
"{} {} install --no-interaction --no-progress > {} 2>&1".format(
|
||
self.project_path, php_bin, composer_bin, log_path,
|
||
)
|
||
)
|
||
public.ExecShell(command)
|
||
vendor_path = os.path.join(self.project_path, "vendor")
|
||
if os.path.isdir(vendor_path):
|
||
# composer 以 root 运行,生成的 vendor 属主是 root,
|
||
# PHP-FPM 以 www 用户运行,不修正会读取失败
|
||
try:
|
||
public.recursive_set_own(vendor_path, "www", "www")
|
||
except Exception:
|
||
pass
|
||
return ""
|
||
tail = public.readFile(log_path) or ""
|
||
return "PHP dependencies may have failed to install; vendor directory was not created: {}".format(tail[-500:])
|
||
|
||
|
||
class StaticCreator(PHPCreator):
|
||
static = True
|