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,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mailpit management for Linux ndev.
|
|
3
|
+
Downloads prebuilt Linux amd64/arm64 binaries from GitHub releases and manages
|
|
4
|
+
the background daemon.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import tarfile
|
|
12
|
+
import time
|
|
13
|
+
import webbrowser
|
|
14
|
+
import httpx
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.progress import Progress, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn, SpinnerColumn
|
|
18
|
+
|
|
19
|
+
from ndev.common.constants import NDEV_DIR, RUN_DIR, LOGS_DIR
|
|
20
|
+
from ndev.common.logger import logger
|
|
21
|
+
from ndev.linux.runtime.process import is_pid_running, read_pid_file, kill_process
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
GITHUB_REPO = "axllent/mailpit"
|
|
26
|
+
RELEASES_API_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
|
|
27
|
+
|
|
28
|
+
BIN_DIR = NDEV_DIR / "bin"
|
|
29
|
+
MAILPIT_BIN = BIN_DIR / "mailpit"
|
|
30
|
+
MAILPIT_PID_FILE = RUN_DIR / "mailpit.pid"
|
|
31
|
+
MAILPIT_PORT_FILE = RUN_DIR / "mailpit.port"
|
|
32
|
+
MAILPIT_LOG_FILE = LOGS_DIR / "mailpit.log"
|
|
33
|
+
MAILPIT_DB_FILE = NDEV_DIR / "mailpit.db"
|
|
34
|
+
|
|
35
|
+
DEFAULT_SMTP_PORT = 1025
|
|
36
|
+
DEFAULT_WEB_PORT = 8025
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_installed() -> bool:
|
|
40
|
+
"""Check if mailpit binary is present."""
|
|
41
|
+
return MAILPIT_BIN.exists() or bool(shutil.which("mailpit"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_binary_path() -> Path:
|
|
45
|
+
if MAILPIT_BIN.exists():
|
|
46
|
+
return MAILPIT_BIN
|
|
47
|
+
system_bin = shutil.which("mailpit")
|
|
48
|
+
if system_bin:
|
|
49
|
+
return Path(system_bin)
|
|
50
|
+
raise FileNotFoundError("Mailpit binary not found. Run `ndev mailpit install` first.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_arch_asset_name() -> str:
|
|
54
|
+
machine = platform.machine().lower()
|
|
55
|
+
if machine in ["x86_64", "amd64"]:
|
|
56
|
+
return "mailpit-linux-amd64.tar.gz"
|
|
57
|
+
elif machine in ["aarch64", "arm64"]:
|
|
58
|
+
return "mailpit-linux-arm64.tar.gz"
|
|
59
|
+
elif machine in ["i386", "i686"]:
|
|
60
|
+
return "mailpit-linux-386.tar.gz"
|
|
61
|
+
elif "arm" in machine:
|
|
62
|
+
return "mailpit-linux-arm.tar.gz"
|
|
63
|
+
return "mailpit-linux-amd64.tar.gz"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def setup_mailpit():
|
|
67
|
+
"""Download and set up prebuilt Mailpit binary if missing."""
|
|
68
|
+
if is_installed():
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
console.print("[bold yellow]Mailpit is not installed. Downloading prebuilt binary...[/bold yellow]")
|
|
72
|
+
BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
temp_dir = NDEV_DIR / "mailpit_temp"
|
|
74
|
+
tar_path = NDEV_DIR / "mailpit.tar.gz"
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Fetch release metadata
|
|
78
|
+
with httpx.Client(timeout=30.0) as client:
|
|
79
|
+
resp = client.get(RELEASES_API_URL, headers={"User-Agent": "ndev/0.1.0"})
|
|
80
|
+
if resp.status_code != 200:
|
|
81
|
+
raise RuntimeError(f"Failed to fetch Mailpit releases. HTTP {resp.status_code}")
|
|
82
|
+
release_data = resp.json()
|
|
83
|
+
|
|
84
|
+
target_asset = _get_arch_asset_name()
|
|
85
|
+
download_url = None
|
|
86
|
+
for asset in release_data.get("assets", []):
|
|
87
|
+
if asset.get("name") == target_asset:
|
|
88
|
+
download_url = asset.get("browser_download_url")
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if not download_url:
|
|
92
|
+
raise RuntimeError(f"Could not find asset '{target_asset}' in Mailpit {release_data.get('tag_name')}")
|
|
93
|
+
|
|
94
|
+
# Download tarball
|
|
95
|
+
with tar_path.open("wb") as f:
|
|
96
|
+
with httpx.stream("GET", download_url, follow_redirects=True, timeout=60.0) as r:
|
|
97
|
+
if r.status_code != 200:
|
|
98
|
+
raise RuntimeError(f"Failed to download Mailpit: HTTP {r.status_code}")
|
|
99
|
+
total = int(r.headers.get("Content-Length", 0))
|
|
100
|
+
with Progress(
|
|
101
|
+
TextColumn("[bold blue]{task.description}"),
|
|
102
|
+
BarColumn(),
|
|
103
|
+
DownloadColumn(),
|
|
104
|
+
TransferSpeedColumn(),
|
|
105
|
+
TimeRemainingColumn(),
|
|
106
|
+
console=console,
|
|
107
|
+
) as progress:
|
|
108
|
+
task = progress.add_task("Downloading Mailpit...", total=total)
|
|
109
|
+
for chunk in r.iter_bytes(chunk_size=16384):
|
|
110
|
+
f.write(chunk)
|
|
111
|
+
progress.update(task, advance=len(chunk))
|
|
112
|
+
|
|
113
|
+
if temp_dir.exists():
|
|
114
|
+
shutil.rmtree(temp_dir)
|
|
115
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
with tarfile.open(tar_path, "r:gz") as tf:
|
|
118
|
+
tf.extractall(temp_dir)
|
|
119
|
+
|
|
120
|
+
extracted_bin = temp_dir / "mailpit"
|
|
121
|
+
if not extracted_bin.exists():
|
|
122
|
+
raise RuntimeError("mailpit binary not found in archive")
|
|
123
|
+
|
|
124
|
+
shutil.move(str(extracted_bin), str(MAILPIT_BIN))
|
|
125
|
+
MAILPIT_BIN.chmod(0o755)
|
|
126
|
+
console.print("[bold green]Mailpit installed successfully![/bold green]\n")
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to install Mailpit: {e}")
|
|
130
|
+
if MAILPIT_BIN.exists():
|
|
131
|
+
MAILPIT_BIN.unlink()
|
|
132
|
+
raise RuntimeError(f"Mailpit setup failed: {e}")
|
|
133
|
+
finally:
|
|
134
|
+
if temp_dir.exists():
|
|
135
|
+
shutil.rmtree(temp_dir)
|
|
136
|
+
if tar_path.exists():
|
|
137
|
+
tar_path.unlink()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def start_mailpit(smtp_port: int = DEFAULT_SMTP_PORT, web_port: int = DEFAULT_WEB_PORT):
|
|
141
|
+
"""Start Mailpit background service."""
|
|
142
|
+
pid = read_pid_file(MAILPIT_PID_FILE)
|
|
143
|
+
if pid and is_pid_running(pid):
|
|
144
|
+
existing_port = MAILPIT_PORT_FILE.read_text().strip() if MAILPIT_PORT_FILE.exists() else str(DEFAULT_WEB_PORT)
|
|
145
|
+
logger.info(f"Mailpit service is already running (PID {pid}, Web UI http://127.0.0.1:{existing_port}).")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
setup_mailpit()
|
|
149
|
+
bin_path = get_binary_path()
|
|
150
|
+
|
|
151
|
+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
log_fd = open(MAILPIT_LOG_FILE, "a")
|
|
155
|
+
|
|
156
|
+
cmd = [
|
|
157
|
+
str(bin_path),
|
|
158
|
+
"--smtp", f"127.0.0.1:{smtp_port}",
|
|
159
|
+
"--listen", f"127.0.0.1:{web_port}",
|
|
160
|
+
"--database", str(MAILPIT_DB_FILE),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
proc = subprocess.Popen(
|
|
164
|
+
cmd,
|
|
165
|
+
stdout=log_fd,
|
|
166
|
+
stderr=log_fd,
|
|
167
|
+
start_new_session=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
time.sleep(0.5)
|
|
171
|
+
if proc.poll() is not None:
|
|
172
|
+
raise RuntimeError(f"Mailpit exited immediately. Check {MAILPIT_LOG_FILE} for details.")
|
|
173
|
+
|
|
174
|
+
MAILPIT_PID_FILE.write_text(str(proc.pid))
|
|
175
|
+
MAILPIT_PORT_FILE.write_text(f"{smtp_port}:{web_port}")
|
|
176
|
+
|
|
177
|
+
logger.info(f"Mailpit service started on http://127.0.0.1:{web_port} (SMTP: 127.0.0.1:{smtp_port}, PID {proc.pid})")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def stop_mailpit():
|
|
181
|
+
"""Stop Mailpit service."""
|
|
182
|
+
pid = read_pid_file(MAILPIT_PID_FILE)
|
|
183
|
+
if not pid or not is_pid_running(pid):
|
|
184
|
+
logger.info("Mailpit service is not running.")
|
|
185
|
+
if MAILPIT_PID_FILE.exists():
|
|
186
|
+
MAILPIT_PID_FILE.unlink()
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
logger.info(f"Stopping Mailpit service (PID {pid})...")
|
|
190
|
+
kill_process(pid)
|
|
191
|
+
if MAILPIT_PID_FILE.exists():
|
|
192
|
+
MAILPIT_PID_FILE.unlink()
|
|
193
|
+
logger.info("Mailpit service stopped.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def restart_mailpit(smtp_port: int = DEFAULT_SMTP_PORT, web_port: int = DEFAULT_WEB_PORT):
|
|
197
|
+
"""Restart Mailpit service."""
|
|
198
|
+
stop_mailpit()
|
|
199
|
+
time.sleep(0.5)
|
|
200
|
+
start_mailpit(smtp_port=smtp_port, web_port=web_port)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_mailpit_status() -> dict:
|
|
204
|
+
"""Get status details for Mailpit service."""
|
|
205
|
+
pid = read_pid_file(MAILPIT_PID_FILE)
|
|
206
|
+
running = is_pid_running(pid) if pid else False
|
|
207
|
+
smtp_port = DEFAULT_SMTP_PORT
|
|
208
|
+
web_port = DEFAULT_WEB_PORT
|
|
209
|
+
|
|
210
|
+
if MAILPIT_PORT_FILE.exists():
|
|
211
|
+
try:
|
|
212
|
+
val = MAILPIT_PORT_FILE.read_text().strip()
|
|
213
|
+
if ":" in val:
|
|
214
|
+
s, w = val.split(":", 1)
|
|
215
|
+
smtp_port = int(s)
|
|
216
|
+
web_port = int(w)
|
|
217
|
+
else:
|
|
218
|
+
web_port = int(val)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
installed = is_installed()
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"service": "mailpit",
|
|
226
|
+
"running": running,
|
|
227
|
+
"pid": pid if running else None,
|
|
228
|
+
"smtp_port": smtp_port,
|
|
229
|
+
"web_port": web_port,
|
|
230
|
+
"url": f"http://127.0.0.1:{web_port}" if running else None,
|
|
231
|
+
"smtp": f"127.0.0.1:{smtp_port}",
|
|
232
|
+
"installed": installed,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def launch_mailpit():
|
|
237
|
+
"""Open Mailpit web UI in default browser, starting it if stopped."""
|
|
238
|
+
status = get_mailpit_status()
|
|
239
|
+
if not status["running"]:
|
|
240
|
+
start_mailpit()
|
|
241
|
+
status = get_mailpit_status()
|
|
242
|
+
url = status["url"] or f"http://127.0.0.1:{DEFAULT_WEB_PORT}"
|
|
243
|
+
console.print(f"Opening [bold cyan]{url}[/bold cyan] in default browser...")
|
|
244
|
+
webbrowser.open(url)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import socket
|
|
4
|
+
import subprocess
|
|
5
|
+
import zipfile
|
|
6
|
+
import secrets
|
|
7
|
+
import httpx
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn, SpinnerColumn
|
|
11
|
+
from ndev.common.constants import NDEV_DIR, CURRENT_LINK, RUN_DIR, LOGS_DIR
|
|
12
|
+
from ndev.common.logger import logger
|
|
13
|
+
from ndev.linux.runtime.process import is_pid_running, read_pid_file, kill_process
|
|
14
|
+
|
|
15
|
+
PMA_DIR = NDEV_DIR / "pma"
|
|
16
|
+
# Backward compatibility migration for legacy phpmyadmin directory
|
|
17
|
+
if not PMA_DIR.exists() and (NDEV_DIR / "phpmyadmin").exists():
|
|
18
|
+
try:
|
|
19
|
+
(NDEV_DIR / "phpmyadmin").rename(PMA_DIR)
|
|
20
|
+
except Exception:
|
|
21
|
+
PMA_DIR = NDEV_DIR / "phpmyadmin"
|
|
22
|
+
|
|
23
|
+
PMA_PID_FILE = RUN_DIR / "pma.pid"
|
|
24
|
+
PMA_PORT_FILE = RUN_DIR / "pma.port"
|
|
25
|
+
PMA_LOG_FILE = LOGS_DIR / "pma.log"
|
|
26
|
+
|
|
27
|
+
def download_file(url: str, dest_path: Path):
|
|
28
|
+
with dest_path.open("wb") as f:
|
|
29
|
+
with httpx.stream("GET", url, follow_redirects=True) as r:
|
|
30
|
+
if r.status_code != 200:
|
|
31
|
+
raise RuntimeError(f"Failed to download phpMyAdmin. HTTP Status Code: {r.status_code}")
|
|
32
|
+
|
|
33
|
+
total = int(r.headers.get("Content-Length", 0))
|
|
34
|
+
|
|
35
|
+
with Progress(
|
|
36
|
+
TextColumn("[bold blue]{task.description}"),
|
|
37
|
+
BarColumn(),
|
|
38
|
+
DownloadColumn(),
|
|
39
|
+
TransferSpeedColumn(),
|
|
40
|
+
TimeRemainingColumn(),
|
|
41
|
+
console=console
|
|
42
|
+
) as progress:
|
|
43
|
+
task = progress.add_task("Downloading phpMyAdmin...", total=total)
|
|
44
|
+
for chunk in r.iter_bytes(chunk_size=16384):
|
|
45
|
+
f.write(chunk)
|
|
46
|
+
progress.update(task, advance=len(chunk))
|
|
47
|
+
|
|
48
|
+
def extract_zip(zip_path: Path, extract_dir: Path):
|
|
49
|
+
with Progress(
|
|
50
|
+
SpinnerColumn(),
|
|
51
|
+
TextColumn("[bold blue]{task.description}"),
|
|
52
|
+
console=console
|
|
53
|
+
) as progress:
|
|
54
|
+
task = progress.add_task("Extracting phpMyAdmin...", total=None)
|
|
55
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
56
|
+
zip_ref.extractall(extract_dir)
|
|
57
|
+
|
|
58
|
+
def find_free_port(start_port=8080) -> int:
|
|
59
|
+
port = start_port
|
|
60
|
+
while port < 65535:
|
|
61
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
62
|
+
try:
|
|
63
|
+
s.bind(("127.0.0.1", port))
|
|
64
|
+
return port
|
|
65
|
+
except socket.error:
|
|
66
|
+
port += 1
|
|
67
|
+
raise RuntimeError("No free ports found.")
|
|
68
|
+
|
|
69
|
+
def setup_pma():
|
|
70
|
+
"""Download and set up phpMyAdmin if not already setup."""
|
|
71
|
+
if PMA_DIR.exists() and (PMA_DIR / "index.php").exists():
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
console.print("[bold yellow]phpMyAdmin is not set up. Installing now...[/bold yellow]")
|
|
75
|
+
temp_dir = NDEV_DIR / "pma_temp"
|
|
76
|
+
zip_path = NDEV_DIR / "phpmyadmin.zip"
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
download_file("https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip", zip_path)
|
|
80
|
+
|
|
81
|
+
if temp_dir.exists():
|
|
82
|
+
shutil.rmtree(temp_dir)
|
|
83
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
extract_zip(zip_path, temp_dir)
|
|
86
|
+
|
|
87
|
+
subfolders = list(temp_dir.glob("phpMyAdmin-*"))
|
|
88
|
+
if not subfolders:
|
|
89
|
+
raise RuntimeError("Could not find extracted phpMyAdmin folder inside zip.")
|
|
90
|
+
src_folder = subfolders[0]
|
|
91
|
+
|
|
92
|
+
if PMA_DIR.exists():
|
|
93
|
+
shutil.rmtree(PMA_DIR)
|
|
94
|
+
PMA_DIR.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
for item in src_folder.iterdir():
|
|
97
|
+
shutil.move(str(item), str(PMA_DIR / item.name))
|
|
98
|
+
|
|
99
|
+
config_path = PMA_DIR / "config.inc.php"
|
|
100
|
+
blowfish_secret = secrets.token_hex(16)
|
|
101
|
+
config_content = f"""<?php
|
|
102
|
+
$cfg['blowfish_secret'] = '{blowfish_secret}';
|
|
103
|
+
$i = 0;
|
|
104
|
+
$i++;
|
|
105
|
+
$cfg['Servers'][$i]['auth_type'] = 'cookie';
|
|
106
|
+
$cfg['Servers'][$i]['host'] = '127.0.0.1';
|
|
107
|
+
$cfg['Servers'][$i]['compress'] = false;
|
|
108
|
+
$cfg['Servers'][$i]['AllowNoPassword'] = true;
|
|
109
|
+
"""
|
|
110
|
+
config_path.write_text(config_content)
|
|
111
|
+
console.print("[bold green]phpMyAdmin setup completed successfully![/bold green]\n")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Failed to set up phpMyAdmin: {e}")
|
|
114
|
+
if temp_dir.exists():
|
|
115
|
+
shutil.rmtree(temp_dir)
|
|
116
|
+
if zip_path.exists():
|
|
117
|
+
zip_path.unlink()
|
|
118
|
+
if PMA_DIR.exists():
|
|
119
|
+
shutil.rmtree(PMA_DIR)
|
|
120
|
+
raise RuntimeError(f"phpMyAdmin setup failed: {e}")
|
|
121
|
+
finally:
|
|
122
|
+
if temp_dir.exists():
|
|
123
|
+
shutil.rmtree(temp_dir)
|
|
124
|
+
if zip_path.exists():
|
|
125
|
+
zip_path.unlink()
|
|
126
|
+
|
|
127
|
+
def start_pma(port: int = None):
|
|
128
|
+
"""Start phpMyAdmin service in background."""
|
|
129
|
+
pid = read_pid_file(PMA_PID_FILE)
|
|
130
|
+
if pid and is_pid_running(pid):
|
|
131
|
+
existing_port = None
|
|
132
|
+
if PMA_PORT_FILE.exists():
|
|
133
|
+
existing_port = PMA_PORT_FILE.read_text().strip()
|
|
134
|
+
logger.info(f"phpMyAdmin service is already running (PID {pid}, Port {existing_port or 'unknown'}).")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
php_path = CURRENT_LINK / "bin" / "php"
|
|
138
|
+
if not php_path.exists():
|
|
139
|
+
system_php = shutil.which("php")
|
|
140
|
+
if system_php:
|
|
141
|
+
php_path = Path(system_php)
|
|
142
|
+
else:
|
|
143
|
+
raise RuntimeError("No active PHP version found. Please run `ndev use <version>` or install PHP first.")
|
|
144
|
+
|
|
145
|
+
setup_pma()
|
|
146
|
+
|
|
147
|
+
if not port:
|
|
148
|
+
if PMA_PORT_FILE.exists():
|
|
149
|
+
try:
|
|
150
|
+
cached_port = int(PMA_PORT_FILE.read_text().strip())
|
|
151
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
152
|
+
s.bind(("127.0.0.1", cached_port))
|
|
153
|
+
port = cached_port
|
|
154
|
+
except Exception:
|
|
155
|
+
port = None
|
|
156
|
+
if not port:
|
|
157
|
+
port = find_free_port(8080)
|
|
158
|
+
|
|
159
|
+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
log_fd = open(PMA_LOG_FILE, "a")
|
|
163
|
+
|
|
164
|
+
cmd = [
|
|
165
|
+
str(php_path),
|
|
166
|
+
"-S", f"127.0.0.1:{port}",
|
|
167
|
+
"-t", str(PMA_DIR)
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
proc = subprocess.Popen(
|
|
171
|
+
cmd,
|
|
172
|
+
cwd=PMA_DIR,
|
|
173
|
+
stdout=log_fd,
|
|
174
|
+
stderr=log_fd,
|
|
175
|
+
start_new_session=True
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
PMA_PID_FILE.write_text(str(proc.pid))
|
|
179
|
+
PMA_PORT_FILE.write_text(str(port))
|
|
180
|
+
|
|
181
|
+
logger.info(f"phpMyAdmin service started on http://127.0.0.1:{port} (PID {proc.pid})")
|
|
182
|
+
|
|
183
|
+
def stop_pma():
|
|
184
|
+
"""Stop phpMyAdmin service."""
|
|
185
|
+
pid = read_pid_file(PMA_PID_FILE)
|
|
186
|
+
if not pid or not is_pid_running(pid):
|
|
187
|
+
logger.info("phpMyAdmin service is not running.")
|
|
188
|
+
if PMA_PID_FILE.exists():
|
|
189
|
+
PMA_PID_FILE.unlink()
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
logger.info(f"Stopping phpMyAdmin service (PID {pid})...")
|
|
193
|
+
kill_process(pid)
|
|
194
|
+
if PMA_PID_FILE.exists():
|
|
195
|
+
PMA_PID_FILE.unlink()
|
|
196
|
+
logger.info("phpMyAdmin service stopped.")
|
|
197
|
+
|
|
198
|
+
def restart_pma(port: int = None):
|
|
199
|
+
"""Restart phpMyAdmin service."""
|
|
200
|
+
stop_pma()
|
|
201
|
+
start_pma(port=port)
|
|
202
|
+
|
|
203
|
+
def get_pma_status() -> dict:
|
|
204
|
+
"""Get status details for phpMyAdmin service."""
|
|
205
|
+
pid = read_pid_file(PMA_PID_FILE)
|
|
206
|
+
running = is_pid_running(pid) if pid else False
|
|
207
|
+
port = None
|
|
208
|
+
if PMA_PORT_FILE.exists():
|
|
209
|
+
try:
|
|
210
|
+
port = int(PMA_PORT_FILE.read_text().strip())
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
installed = PMA_DIR.exists() and (PMA_DIR / "index.php").exists()
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"service": "pma",
|
|
218
|
+
"running": running,
|
|
219
|
+
"pid": pid if running else None,
|
|
220
|
+
"port": port,
|
|
221
|
+
"url": f"http://127.0.0.1:{port}" if port and running else None,
|
|
222
|
+
"installed": installed
|
|
223
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def is_pid_running(pid: int) -> bool:
|
|
7
|
+
"""Check if a process is running on POSIX systems."""
|
|
8
|
+
if pid <= 0:
|
|
9
|
+
return False
|
|
10
|
+
try:
|
|
11
|
+
os.kill(pid, 0)
|
|
12
|
+
return True
|
|
13
|
+
except OSError:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
def read_pid_file(pid_file: Path) -> int | None:
|
|
17
|
+
"""Read a PID from a file."""
|
|
18
|
+
if not pid_file.exists():
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
content = pid_file.read_text().strip()
|
|
22
|
+
if content.isdigit():
|
|
23
|
+
return int(content)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def kill_process(pid: int, sig=signal.SIGTERM) -> bool:
|
|
29
|
+
"""Send signal to process. Return True if successful."""
|
|
30
|
+
try:
|
|
31
|
+
os.kill(pid, sig)
|
|
32
|
+
return True
|
|
33
|
+
except ProcessLookupError:
|
|
34
|
+
return False
|
|
35
|
+
except PermissionError as e:
|
|
36
|
+
logger.error(f"Permission denied to signal process {pid}: {e}")
|
|
37
|
+
return False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from ndev.common.constants import RUN_DIR
|
|
3
|
+
|
|
4
|
+
def get_major_minor(version: str) -> str:
|
|
5
|
+
parts = version.split(".")
|
|
6
|
+
return f"{parts[0]}{parts[1]}"
|
|
7
|
+
|
|
8
|
+
def get_socket_path(version: str) -> Path:
|
|
9
|
+
"""Return the UNIX socket path for a PHP version."""
|
|
10
|
+
mm = get_major_minor(version)
|
|
11
|
+
return RUN_DIR / f"php{mm}.sock"
|
|
12
|
+
|
|
13
|
+
def get_pid_path(version: str) -> Path:
|
|
14
|
+
"""Return the PID file path for a PHP-FPM daemon version."""
|
|
15
|
+
mm = get_major_minor(version)
|
|
16
|
+
return RUN_DIR / f"php-fpm-{mm}.pid"
|