ndev-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ndev/__init__.py +8 -0
- ndev/__main__.py +4 -0
- ndev/cli.py +24 -0
- ndev/common/__init__.py +3 -0
- ndev/common/config.py +114 -0
- ndev/common/constants.py +51 -0
- ndev/common/github.py +13 -0
- ndev/common/logger.py +11 -0
- ndev/common/manifest.py +41 -0
- ndev/common/utils.py +96 -0
- ndev/linux/__init__.py +1 -0
- ndev/linux/chroot/manager.py +63 -0
- ndev/linux/chroot/packages.py +91 -0
- ndev/linux/chroot/shell.py +9 -0
- ndev/linux/cli.py +235 -0
- ndev/linux/commands/available.py +38 -0
- ndev/linux/commands/clean.py +25 -0
- ndev/linux/commands/ctl.py +192 -0
- ndev/linux/commands/current.py +11 -0
- ndev/linux/commands/db.py +319 -0
- ndev/linux/commands/doctor.py +56 -0
- ndev/linux/commands/grok.py +75 -0
- ndev/linux/commands/install.py +39 -0
- ndev/linux/commands/list.py +47 -0
- ndev/linux/commands/logs.py +36 -0
- ndev/linux/commands/mailpit.py +82 -0
- ndev/linux/commands/reload.py +26 -0
- ndev/linux/commands/restart.py +34 -0
- ndev/linux/commands/setup.py +113 -0
- ndev/linux/commands/start.py +34 -0
- ndev/linux/commands/status.py +81 -0
- ndev/linux/commands/stop.py +34 -0
- ndev/linux/commands/uninstall.py +69 -0
- ndev/linux/commands/update.py +57 -0
- ndev/linux/commands/upgrade.py +81 -0
- ndev/linux/commands/use.py +108 -0
- ndev/linux/commands/vhost.py +350 -0
- ndev/linux/php/builder.py +183 -0
- ndev/linux/php/downloader.py +58 -0
- ndev/linux/php/extensions.py +146 -0
- ndev/linux/php/installer.py +42 -0
- ndev/linux/php/resolver.py +59 -0
- ndev/linux/php/templates.py +128 -0
- ndev/linux/runtime/fpm.py +117 -0
- ndev/linux/runtime/mailpit.py +244 -0
- ndev/linux/runtime/pma.py +223 -0
- ndev/linux/runtime/process.py +37 -0
- ndev/linux/runtime/sockets.py +16 -0
- ndev/linux/runtime/upgrade.py +431 -0
- ndev/linux/tui.py +1423 -0
- ndev/main.py +52 -0
- ndev/tui.py +23 -0
- ndev/win/__init__.py +1 -0
- ndev/win/cli.py +1898 -0
- ndev/win/commands/__init__.py +0 -0
- ndev/win/core/__init__.py +0 -0
- ndev/win/core/db.py +265 -0
- ndev/win/core/elevate.py +94 -0
- ndev/win/core/ext.py +241 -0
- ndev/win/core/fcgi.py +216 -0
- ndev/win/core/grok.py +55 -0
- ndev/win/core/logs.py +66 -0
- ndev/win/core/mailpit.py +236 -0
- ndev/win/core/mkcert.py +65 -0
- ndev/win/core/paths.py +85 -0
- ndev/win/core/php.py +533 -0
- ndev/win/core/pma.py +190 -0
- ndev/win/core/services.py +349 -0
- ndev/win/core/setup.py +361 -0
- ndev/win/core/upgrade.py +513 -0
- ndev/win/core/vhost.py +289 -0
- ndev/win/templates/vhost.conf.tmpl +33 -0
- ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
- ndev/win/tui.py +1313 -0
- ndev_stack-0.1.0.dist-info/METADATA +553 -0
- ndev_stack-0.1.0.dist-info/RECORD +79 -0
- ndev_stack-0.1.0.dist-info/WHEEL +5 -0
- ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
- ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import tarfile
|
|
3
|
+
import subprocess
|
|
4
|
+
import httpx
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ndev.common.constants import PHP_DIR, DOWNLOADS_DIR, BUILDS_DIR
|
|
7
|
+
from ndev.linux.chroot.manager import SandboxManager
|
|
8
|
+
from ndev.common.logger import logger
|
|
9
|
+
from ndev.common.utils import run_command
|
|
10
|
+
|
|
11
|
+
def get_php_binaries(version: str) -> tuple[Path, Path, Path]:
|
|
12
|
+
"""Get the path to php, phpize, and php-config binaries for a version."""
|
|
13
|
+
prefix = PHP_DIR / version
|
|
14
|
+
php_bin = prefix / "bin" / "php"
|
|
15
|
+
phpize_bin = prefix / "bin" / "phpize"
|
|
16
|
+
php_config_bin = prefix / "bin" / "php-config"
|
|
17
|
+
return php_bin, phpize_bin, php_config_bin
|
|
18
|
+
|
|
19
|
+
def list_extensions(version: str) -> list[str]:
|
|
20
|
+
"""List loaded PHP extensions by running php -m."""
|
|
21
|
+
php_bin, _, _ = get_php_binaries(version)
|
|
22
|
+
if not php_bin.exists():
|
|
23
|
+
raise ValueError(f"PHP version {version} is not installed.")
|
|
24
|
+
|
|
25
|
+
res = subprocess.run([str(php_bin), "-m"], capture_output=True, text=True)
|
|
26
|
+
if res.returncode != 0:
|
|
27
|
+
raise ValueError(f"Failed to list extensions: {res.stderr}")
|
|
28
|
+
|
|
29
|
+
lines = res.stdout.splitlines()
|
|
30
|
+
extensions = []
|
|
31
|
+
for line in lines:
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if line and not line.startswith("["):
|
|
34
|
+
extensions.append(line)
|
|
35
|
+
return sorted(extensions)
|
|
36
|
+
|
|
37
|
+
def enable_extension(version: str, ext_name: str):
|
|
38
|
+
"""Enable an extension by creating its ini file in etc/conf.d/."""
|
|
39
|
+
prefix = PHP_DIR / version
|
|
40
|
+
conf_d = prefix / "etc" / "conf.d"
|
|
41
|
+
conf_d.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
ini_file = conf_d / f"{ext_name}.ini"
|
|
44
|
+
|
|
45
|
+
# OPcache and Xdebug require zend_extension, others require extension
|
|
46
|
+
if ext_name.lower() in ["opcache", "xdebug"]:
|
|
47
|
+
ini_file.write_text(f"zend_extension={ext_name}\n")
|
|
48
|
+
else:
|
|
49
|
+
ini_file.write_text(f"extension={ext_name}\n")
|
|
50
|
+
|
|
51
|
+
logger.info(f"Extension '{ext_name}' enabled for PHP {version} (ini file created at {ini_file}).")
|
|
52
|
+
|
|
53
|
+
def disable_extension(version: str, ext_name: str):
|
|
54
|
+
"""Disable an extension by removing its ini file in etc/conf.d/."""
|
|
55
|
+
prefix = PHP_DIR / version
|
|
56
|
+
ini_file = prefix / "etc" / "conf.d" / f"{ext_name}.ini"
|
|
57
|
+
if ini_file.exists():
|
|
58
|
+
ini_file.unlink()
|
|
59
|
+
logger.info(f"Extension '{ext_name}' disabled for PHP {version} (ini file removed).")
|
|
60
|
+
else:
|
|
61
|
+
logger.warning(f"Extension '{ext_name}' was not enabled (ini file {ini_file} does not exist).")
|
|
62
|
+
|
|
63
|
+
def install_extension(version: str, ext_name: str, show_logs: bool = False):
|
|
64
|
+
"""Download, compile, and install a PECL extension inside the sandbox."""
|
|
65
|
+
php_bin, phpize_bin, php_config_bin = get_php_binaries(version)
|
|
66
|
+
if not php_bin.exists():
|
|
67
|
+
raise ValueError(f"PHP version {version} is not installed.")
|
|
68
|
+
if not phpize_bin.exists():
|
|
69
|
+
raise ValueError(f"phpize binary not found for version {version}. Development headers may be missing.")
|
|
70
|
+
|
|
71
|
+
# 1. Download extension archive on host
|
|
72
|
+
DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
archive_path = DOWNLOADS_DIR / f"{ext_name}.tgz"
|
|
74
|
+
url = f"https://pecl.php.net/get/{ext_name}"
|
|
75
|
+
|
|
76
|
+
logger.info(f"Downloading extension '{ext_name}' from {url}...")
|
|
77
|
+
with httpx.Client(follow_redirects=True) as client:
|
|
78
|
+
res = client.get(url)
|
|
79
|
+
if res.status_code != 200:
|
|
80
|
+
raise ValueError(f"Failed to download extension '{ext_name}' from PECL (HTTP status {res.status_code}).")
|
|
81
|
+
archive_path.write_bytes(res.content)
|
|
82
|
+
|
|
83
|
+
# 2. Extract extension archive
|
|
84
|
+
BUILDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
build_dir = BUILDS_DIR / f"ext-{ext_name}"
|
|
86
|
+
if build_dir.exists():
|
|
87
|
+
if build_dir.is_dir():
|
|
88
|
+
shutil.rmtree(build_dir)
|
|
89
|
+
else:
|
|
90
|
+
build_dir.unlink()
|
|
91
|
+
|
|
92
|
+
logger.info(f"Extracting extension archive to {build_dir}...")
|
|
93
|
+
with tarfile.open(archive_path) as tar:
|
|
94
|
+
root_dir_name = None
|
|
95
|
+
for member in tar.getmembers():
|
|
96
|
+
if member.name.endswith("config.m4"):
|
|
97
|
+
parts = Path(member.name).parts
|
|
98
|
+
if len(parts) > 1:
|
|
99
|
+
root_dir_name = parts[0]
|
|
100
|
+
break
|
|
101
|
+
if not root_dir_name:
|
|
102
|
+
for member in tar.getmembers():
|
|
103
|
+
parts = Path(member.name).parts
|
|
104
|
+
if len(parts) > 1 and parts[0] != ".":
|
|
105
|
+
root_dir_name = parts[0]
|
|
106
|
+
break
|
|
107
|
+
if not root_dir_name:
|
|
108
|
+
raise ValueError("Could not find a valid root directory in extension archive.")
|
|
109
|
+
tar.extractall(path=BUILDS_DIR)
|
|
110
|
+
|
|
111
|
+
extracted_path = BUILDS_DIR / root_dir_name
|
|
112
|
+
if extracted_path.resolve() != build_dir.resolve():
|
|
113
|
+
shutil.move(str(extracted_path), str(build_dir))
|
|
114
|
+
|
|
115
|
+
# 3. Configure and compile inside sandbox
|
|
116
|
+
if not show_logs:
|
|
117
|
+
logger.info(f"[yellow]Compiling extension '{ext_name}'...[/yellow]")
|
|
118
|
+
else:
|
|
119
|
+
logger.info(f"Compiling extension '{ext_name}' inside the sandbox...")
|
|
120
|
+
sandbox = SandboxManager()
|
|
121
|
+
|
|
122
|
+
# Run phpize
|
|
123
|
+
sandbox.run([str(phpize_bin)], cwd=build_dir, show_logs=show_logs)
|
|
124
|
+
|
|
125
|
+
# Run configure
|
|
126
|
+
sandbox.run(["./configure", f"--with-php-config={php_config_bin}"], cwd=build_dir, show_logs=show_logs)
|
|
127
|
+
|
|
128
|
+
# Run make
|
|
129
|
+
sandbox.run(["make", "-j4"], cwd=build_dir, show_logs=show_logs)
|
|
130
|
+
|
|
131
|
+
if not show_logs:
|
|
132
|
+
logger.info(f"[green]Compiled extension '{ext_name}'[/green]")
|
|
133
|
+
logger.info(f"[yellow]Installing extension '{ext_name}'...[/yellow]")
|
|
134
|
+
else:
|
|
135
|
+
logger.info("Installing extension...")
|
|
136
|
+
sandbox.run(["make", "install"], cwd=build_dir, show_logs=show_logs)
|
|
137
|
+
|
|
138
|
+
if not show_logs:
|
|
139
|
+
logger.info(f"[green]Installed extension '{ext_name}'[/green]")
|
|
140
|
+
|
|
141
|
+
# 4. Enable extension (strip version suffix if present: e.g. xdebug-3.1.6 -> xdebug)
|
|
142
|
+
base_ext_name = ext_name.split("-")[0]
|
|
143
|
+
enable_extension(version, base_ext_name)
|
|
144
|
+
|
|
145
|
+
# Clean up build dir
|
|
146
|
+
shutil.rmtree(build_dir, ignore_errors=True)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from ndev.linux.php.resolver import resolve_version
|
|
3
|
+
from ndev.linux.php.downloader import download_php_source
|
|
4
|
+
from ndev.linux.php.builder import build_php
|
|
5
|
+
from ndev.linux.php.templates import write_default_configs
|
|
6
|
+
from ndev.linux.chroot.packages import install_host_packages
|
|
7
|
+
from ndev.common.logger import logger
|
|
8
|
+
|
|
9
|
+
def install_version(version_input: str, show_logs: bool = False) -> str:
|
|
10
|
+
"""Resolve, download, build and configure a PHP version."""
|
|
11
|
+
resolved_version, filename, sha256, download_url = resolve_version(version_input)
|
|
12
|
+
|
|
13
|
+
archive_path = download_php_source(filename, sha256, download_url)
|
|
14
|
+
|
|
15
|
+
# Pre-install development dependencies inside sandbox
|
|
16
|
+
install_host_packages([
|
|
17
|
+
"libsqlite3-dev",
|
|
18
|
+
"libonig-dev",
|
|
19
|
+
"libcrypt-dev",
|
|
20
|
+
"libcurl4-openssl-dev",
|
|
21
|
+
"libxml2-dev",
|
|
22
|
+
"libssl-dev",
|
|
23
|
+
"libzip-dev",
|
|
24
|
+
"libicu-dev",
|
|
25
|
+
"libsodium-dev",
|
|
26
|
+
"libpng-dev",
|
|
27
|
+
"libjpeg-dev",
|
|
28
|
+
"libwebp-dev",
|
|
29
|
+
"libwebpdecoder3",
|
|
30
|
+
"libfreetype-dev",
|
|
31
|
+
"libbz2-dev",
|
|
32
|
+
"libgmp-dev",
|
|
33
|
+
"libreadline-dev",
|
|
34
|
+
"zlib1g-dev"
|
|
35
|
+
], show_logs=show_logs)
|
|
36
|
+
|
|
37
|
+
install_prefix = build_php(resolved_version, archive_path, show_logs=show_logs)
|
|
38
|
+
|
|
39
|
+
write_default_configs(install_prefix, resolved_version)
|
|
40
|
+
|
|
41
|
+
logger.info(f"Successfully finished installation of PHP version {resolved_version}")
|
|
42
|
+
return resolved_version
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from packaging.version import parse as parse_version
|
|
3
|
+
from ndev.common.github import fetch_releases
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def resolve_version(version_input: str) -> tuple[str, str, str, str]:
|
|
7
|
+
"""
|
|
8
|
+
Resolve version input (e.g. '8.4', '8.5.8') to:
|
|
9
|
+
(exact_version, filename, sha256, download_url)
|
|
10
|
+
"""
|
|
11
|
+
version_input = version_input.strip().lower()
|
|
12
|
+
if version_input.startswith("php-"):
|
|
13
|
+
version_input = version_input[4:]
|
|
14
|
+
|
|
15
|
+
parts = version_input.split(".")
|
|
16
|
+
try:
|
|
17
|
+
major = int(parts[0])
|
|
18
|
+
except ValueError:
|
|
19
|
+
raise ValueError(f"Invalid version format: '{version_input}'")
|
|
20
|
+
|
|
21
|
+
logger.info(f"Resolving version prefix '{version_input}'...")
|
|
22
|
+
releases = fetch_releases(major)
|
|
23
|
+
if not releases:
|
|
24
|
+
raise ValueError(f"No releases found or network error for PHP version {major}")
|
|
25
|
+
|
|
26
|
+
matching_versions = []
|
|
27
|
+
for v in releases.keys():
|
|
28
|
+
if v.startswith(version_input):
|
|
29
|
+
matching_versions.append(v)
|
|
30
|
+
|
|
31
|
+
if not matching_versions:
|
|
32
|
+
raise ValueError(f"Could not resolve version prefix '{version_input}' to any active release.")
|
|
33
|
+
|
|
34
|
+
resolved_version = max(matching_versions, key=parse_version)
|
|
35
|
+
logger.info(f"Resolved to version: {resolved_version}")
|
|
36
|
+
|
|
37
|
+
release_data = releases[resolved_version]
|
|
38
|
+
sources = release_data.get("source", [])
|
|
39
|
+
|
|
40
|
+
chosen_source = None
|
|
41
|
+
for ext in [".tar.xz", ".tar.gz", ".tar.bz2"]:
|
|
42
|
+
for src in sources:
|
|
43
|
+
if src.get("filename", "").endswith(ext):
|
|
44
|
+
chosen_source = src
|
|
45
|
+
break
|
|
46
|
+
if chosen_source:
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
if not chosen_source:
|
|
50
|
+
if sources:
|
|
51
|
+
chosen_source = sources[0]
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"No source downloads available for resolved version {resolved_version}")
|
|
54
|
+
|
|
55
|
+
filename = chosen_source["filename"]
|
|
56
|
+
sha256 = chosen_source.get("sha256", "")
|
|
57
|
+
download_url = f"https://www.php.net/distributions/{filename}"
|
|
58
|
+
|
|
59
|
+
return resolved_version, filename, sha256, download_url
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import getpass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from jinja2 import Template
|
|
5
|
+
from ndev.common.constants import RUN_DIR, LOGS_DIR
|
|
6
|
+
from ndev.common.logger import logger
|
|
7
|
+
|
|
8
|
+
PHP_INI_TEMPLATE = """[PHP]
|
|
9
|
+
engine = On
|
|
10
|
+
short_open_tag = Off
|
|
11
|
+
precision = 14
|
|
12
|
+
output_buffering = 4096
|
|
13
|
+
zlib.output_compression = Off
|
|
14
|
+
implicit_flush = Off
|
|
15
|
+
unserialize_callback_func =
|
|
16
|
+
serialize_precision = -1
|
|
17
|
+
disable_functions =
|
|
18
|
+
disable_classes =
|
|
19
|
+
zend.enable_gc = On
|
|
20
|
+
expose_php = On
|
|
21
|
+
max_execution_time = 30
|
|
22
|
+
max_input_time = 60
|
|
23
|
+
memory_limit = 128M
|
|
24
|
+
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
|
|
25
|
+
display_errors = On
|
|
26
|
+
display_startup_errors = On
|
|
27
|
+
log_errors = On
|
|
28
|
+
log_errors_max_len = 1024
|
|
29
|
+
ignore_repeated_errors = Off
|
|
30
|
+
ignore_repeated_source = Off
|
|
31
|
+
report_memleaks = On
|
|
32
|
+
html_errors = On
|
|
33
|
+
variables_order = "GPCS"
|
|
34
|
+
request_order = "GP"
|
|
35
|
+
register_argc_argv = Off
|
|
36
|
+
auto_globals_jit = On
|
|
37
|
+
post_max_size = 8M
|
|
38
|
+
default_mimetype = "text/html"
|
|
39
|
+
default_charset = "UTF-8"
|
|
40
|
+
doc_root =
|
|
41
|
+
user_dir =
|
|
42
|
+
enable_dl = Off
|
|
43
|
+
file_uploads = On
|
|
44
|
+
upload_max_filesize = 2M
|
|
45
|
+
max_file_uploads = 20
|
|
46
|
+
allow_url_fopen = On
|
|
47
|
+
allow_url_include = Off
|
|
48
|
+
default_socket_timeout = 60
|
|
49
|
+
|
|
50
|
+
[CLI Server]
|
|
51
|
+
cli_server.color = On
|
|
52
|
+
|
|
53
|
+
[Date]
|
|
54
|
+
date.timezone = UTC
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
PHP_FPM_CONF_TEMPLATE = """[global]
|
|
58
|
+
pid = {{ run_dir }}/php-fpm-{{ major_minor }}.pid
|
|
59
|
+
error_log = {{ logs_dir }}/php-fpm-{{ major_minor }}.log
|
|
60
|
+
log_level = notice
|
|
61
|
+
include = {{ prefix }}/etc/php-fpm.d/*.conf
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
WWW_CONF_TEMPLATE = """[www]
|
|
65
|
+
user = {{ user }}
|
|
66
|
+
group = {{ group }}
|
|
67
|
+
listen = {{ run_dir }}/php{{ major_minor }}.sock
|
|
68
|
+
listen.owner = {{ user }}
|
|
69
|
+
listen.group = {{ group }}
|
|
70
|
+
listen.mode = 0666
|
|
71
|
+
|
|
72
|
+
pm = dynamic
|
|
73
|
+
pm.max_children = 5
|
|
74
|
+
pm.start_servers = 2
|
|
75
|
+
pm.min_spare_servers = 1
|
|
76
|
+
pm.max_spare_servers = 3
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def get_major_minor(version: str) -> str:
|
|
80
|
+
parts = version.split(".")
|
|
81
|
+
return f"{parts[0]}{parts[1]}"
|
|
82
|
+
|
|
83
|
+
def write_default_configs(prefix: Path, version: str):
|
|
84
|
+
"""Generate and write default configs to the installed PHP prefix."""
|
|
85
|
+
etc_dir = prefix / "etc"
|
|
86
|
+
fpm_d = etc_dir / "php-fpm.d"
|
|
87
|
+
conf_d = etc_dir / "conf.d"
|
|
88
|
+
|
|
89
|
+
# Create directories
|
|
90
|
+
etc_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
fpm_d.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
conf_d.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
# Get current user details
|
|
95
|
+
user = getpass.getuser()
|
|
96
|
+
# On some systems, group matches user
|
|
97
|
+
group = user
|
|
98
|
+
|
|
99
|
+
major_minor = get_major_minor(version)
|
|
100
|
+
|
|
101
|
+
context = {
|
|
102
|
+
"prefix": str(prefix),
|
|
103
|
+
"run_dir": str(RUN_DIR),
|
|
104
|
+
"logs_dir": str(LOGS_DIR),
|
|
105
|
+
"major_minor": major_minor,
|
|
106
|
+
"user": user,
|
|
107
|
+
"group": group
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# 1. php.ini
|
|
111
|
+
php_ini_path = etc_dir / "php.ini"
|
|
112
|
+
if not php_ini_path.exists():
|
|
113
|
+
php_ini_path.write_text(PHP_INI_TEMPLATE)
|
|
114
|
+
logger.info(f"Generated default php.ini at {php_ini_path}")
|
|
115
|
+
|
|
116
|
+
# 2. php-fpm.conf
|
|
117
|
+
php_fpm_conf_path = etc_dir / "php-fpm.conf"
|
|
118
|
+
if not php_fpm_conf_path.exists():
|
|
119
|
+
rendered = Template(PHP_FPM_CONF_TEMPLATE).render(context)
|
|
120
|
+
php_fpm_conf_path.write_text(rendered)
|
|
121
|
+
logger.info(f"Generated default php-fpm.conf at {php_fpm_conf_path}")
|
|
122
|
+
|
|
123
|
+
# 3. www.conf
|
|
124
|
+
www_conf_path = fpm_d / "www.conf"
|
|
125
|
+
if not www_conf_path.exists():
|
|
126
|
+
rendered = Template(WWW_CONF_TEMPLATE).render(context)
|
|
127
|
+
www_conf_path.write_text(rendered)
|
|
128
|
+
logger.info(f"Generated default www.conf at {www_conf_path}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import signal
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
from ndev.common.constants import PHP_DIR
|
|
6
|
+
from ndev.common.logger import logger
|
|
7
|
+
from ndev.linux.runtime.process import is_pid_running, read_pid_file, kill_process
|
|
8
|
+
from ndev.linux.runtime.sockets import get_pid_path, get_socket_path
|
|
9
|
+
|
|
10
|
+
def get_fpm_binary(version: str) -> Path:
|
|
11
|
+
"""Get the path to php-fpm binary for a version."""
|
|
12
|
+
prefix = PHP_DIR / version
|
|
13
|
+
return prefix / "sbin" / "php-fpm"
|
|
14
|
+
|
|
15
|
+
def start_fpm(version: str):
|
|
16
|
+
"""Start PHP-FPM daemon for a version."""
|
|
17
|
+
pid_file = get_pid_path(version)
|
|
18
|
+
pid = read_pid_file(pid_file)
|
|
19
|
+
|
|
20
|
+
if pid and is_pid_running(pid):
|
|
21
|
+
logger.info(f"PHP-FPM {version} is already running with PID {pid}.")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
fpm_bin = get_fpm_binary(version)
|
|
25
|
+
if not fpm_bin.exists():
|
|
26
|
+
raise FileNotFoundError(f"PHP-FPM binary not found at {fpm_bin} for version {version}")
|
|
27
|
+
|
|
28
|
+
prefix = PHP_DIR / version
|
|
29
|
+
conf_file = prefix / "etc" / "php-fpm.conf"
|
|
30
|
+
ini_file = prefix / "etc" / "php.ini"
|
|
31
|
+
|
|
32
|
+
cmd = [
|
|
33
|
+
str(fpm_bin),
|
|
34
|
+
"-y", str(conf_file),
|
|
35
|
+
"-c", str(ini_file)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
logger.info(f"Starting PHP-FPM {version}...")
|
|
39
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
40
|
+
if res.returncode != 0:
|
|
41
|
+
raise RuntimeError(f"Failed to start PHP-FPM {version}: {res.stderr or res.stdout}")
|
|
42
|
+
|
|
43
|
+
for _ in range(10):
|
|
44
|
+
time.sleep(0.2)
|
|
45
|
+
pid = read_pid_file(pid_file)
|
|
46
|
+
if pid and is_pid_running(pid):
|
|
47
|
+
logger.info(f"PHP-FPM {version} started successfully (PID {pid}).")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
logger.warning(f"PHP-FPM {version} launched, but PID file could not be verified.")
|
|
51
|
+
|
|
52
|
+
def stop_fpm(version: str):
|
|
53
|
+
"""Stop PHP-FPM daemon for a version."""
|
|
54
|
+
pid_file = get_pid_path(version)
|
|
55
|
+
pid = read_pid_file(pid_file)
|
|
56
|
+
|
|
57
|
+
if not pid or not is_pid_running(pid):
|
|
58
|
+
logger.info(f"PHP-FPM {version} is not running.")
|
|
59
|
+
if pid_file.exists():
|
|
60
|
+
pid_file.unlink()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
logger.info(f"Stopping PHP-FPM {version} (PID {pid})...")
|
|
64
|
+
kill_process(pid, signal.SIGTERM)
|
|
65
|
+
|
|
66
|
+
for _ in range(20):
|
|
67
|
+
time.sleep(0.2)
|
|
68
|
+
if not is_pid_running(pid):
|
|
69
|
+
logger.info(f"PHP-FPM {version} stopped.")
|
|
70
|
+
if pid_file.exists():
|
|
71
|
+
pid_file.unlink()
|
|
72
|
+
sock_file = get_socket_path(version)
|
|
73
|
+
if sock_file.exists():
|
|
74
|
+
sock_file.unlink()
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
logger.warning("FPM process did not exit. Force killing...")
|
|
78
|
+
kill_process(pid, signal.SIGKILL)
|
|
79
|
+
if pid_file.exists():
|
|
80
|
+
pid_file.unlink()
|
|
81
|
+
sock_file = get_socket_path(version)
|
|
82
|
+
if sock_file.exists():
|
|
83
|
+
sock_file.unlink()
|
|
84
|
+
|
|
85
|
+
def reload_fpm(version: str):
|
|
86
|
+
"""Gracefully reload PHP-FPM daemon (SIGUSR2)."""
|
|
87
|
+
pid_file = get_pid_path(version)
|
|
88
|
+
pid = read_pid_file(pid_file)
|
|
89
|
+
|
|
90
|
+
if not pid or not is_pid_running(pid):
|
|
91
|
+
logger.warning(f"PHP-FPM {version} is not running. Starting it instead...")
|
|
92
|
+
start_fpm(version)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
logger.info(f"Reloading PHP-FPM {version} (PID {pid})...")
|
|
96
|
+
kill_process(pid, signal.SIGUSR2)
|
|
97
|
+
logger.info("SIGUSR2 reload signal sent.")
|
|
98
|
+
|
|
99
|
+
def restart_fpm(version: str):
|
|
100
|
+
"""Restart PHP-FPM daemon."""
|
|
101
|
+
stop_fpm(version)
|
|
102
|
+
start_fpm(version)
|
|
103
|
+
|
|
104
|
+
def get_fpm_status(version: str) -> dict:
|
|
105
|
+
"""Get the status of PHP-FPM for a version."""
|
|
106
|
+
pid_file = get_pid_path(version)
|
|
107
|
+
pid = read_pid_file(pid_file)
|
|
108
|
+
running = is_pid_running(pid) if pid else False
|
|
109
|
+
socket_path = get_socket_path(version)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"version": version,
|
|
113
|
+
"pid": pid if running else None,
|
|
114
|
+
"running": running,
|
|
115
|
+
"socket": str(socket_path) if socket_path.exists() else str(socket_path),
|
|
116
|
+
"socket_exists": socket_path.exists()
|
|
117
|
+
}
|