ndev-stack 0.3.10__tar.gz → 0.3.12__tar.gz
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_stack-0.3.10 → ndev_stack-0.3.12}/PKG-INFO +1 -1
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/__init__.py +1 -1
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/vhost.py +18 -5
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/nginx_helper.py +91 -7
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/tui.py +29 -12
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/PKG-INFO +1 -1
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/pyproject.toml +1 -1
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/tests/test_opensuse_compat.py +59 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/README.md +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/cli.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/distro.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mongodb/module.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/postgres/module.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/doctor.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/grok.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/setup.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/services.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/main.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/tui.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/cli.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/fcgi.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/grok.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/php.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/redis_core.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/setup.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/shim.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/upgrade.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/vhost.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/templates/vhost.conf.tmpl +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/SOURCES.txt +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/setup.cfg +0 -0
- {ndev_stack-0.3.10 → ndev_stack-0.3.12}/tests/test_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -319,6 +319,8 @@ def vhost_cmd(
|
|
|
319
319
|
get_nginx_vhost_dirs,
|
|
320
320
|
generate_vhost_config,
|
|
321
321
|
ensure_nginx_sites_enabled_included,
|
|
322
|
+
install_nginx_ssl_cert,
|
|
323
|
+
ensure_docroot_accessible,
|
|
322
324
|
add_to_hosts_privileged,
|
|
323
325
|
reload_nginx,
|
|
324
326
|
)
|
|
@@ -338,14 +340,25 @@ def vhost_cmd(
|
|
|
338
340
|
if enabled_dir:
|
|
339
341
|
ensure_nginx_sites_enabled_included()
|
|
340
342
|
|
|
343
|
+
# Install SSL certificates into /etc/nginx/ssl for systemd / SELinux compatibility
|
|
344
|
+
final_cert_path = cert_path
|
|
345
|
+
final_key_path = key_path
|
|
346
|
+
if ssl and cert_path and key_path:
|
|
347
|
+
try:
|
|
348
|
+
final_cert_path, final_key_path = install_nginx_ssl_cert(domain, cert_path, key_path)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.warning(f"Could not install SSL certificate to /etc/nginx/ssl: {e}")
|
|
351
|
+
|
|
352
|
+
ensure_docroot_accessible(root_path)
|
|
353
|
+
|
|
341
354
|
conf_file = vhost_dir / f"{domain}.conf"
|
|
342
355
|
config_template = generate_vhost_config(
|
|
343
356
|
domain=domain,
|
|
344
|
-
root=
|
|
357
|
+
root=str(root_path),
|
|
345
358
|
selected_sock=selected_sock,
|
|
346
359
|
ssl=bool(ssl),
|
|
347
|
-
cert_path=
|
|
348
|
-
key_path=
|
|
360
|
+
cert_path=final_cert_path,
|
|
361
|
+
key_path=final_key_path,
|
|
349
362
|
)
|
|
350
363
|
|
|
351
364
|
enabled_link = None
|
|
@@ -382,8 +395,8 @@ def vhost_cmd(
|
|
|
382
395
|
if enabled_link:
|
|
383
396
|
console.print(f"Symlink : {enabled_link}")
|
|
384
397
|
if ssl:
|
|
385
|
-
console.print(f"SSL Cert : {
|
|
386
|
-
console.print(f"SSL Key : {
|
|
398
|
+
console.print(f"SSL Cert : {final_cert_path}")
|
|
399
|
+
console.print(f"SSL Key : {final_key_path}")
|
|
387
400
|
console.print(f"\nOpen: https://{domain}")
|
|
388
401
|
else:
|
|
389
402
|
console.print(f"\nOpen: http://{domain}")
|
|
@@ -426,13 +426,24 @@ server {{
|
|
|
426
426
|
|
|
427
427
|
def reload_nginx(password: Optional[str] = None) -> bool:
|
|
428
428
|
"""
|
|
429
|
-
Reload Nginx configuration
|
|
429
|
+
Reload Nginx configuration if active, or start Nginx if stopped.
|
|
430
430
|
"""
|
|
431
|
+
if os.geteuid() != 0 and password:
|
|
432
|
+
validate_sudo_password(password)
|
|
433
|
+
|
|
431
434
|
cmd = None
|
|
432
435
|
if shutil.which("systemctl"):
|
|
433
|
-
|
|
436
|
+
res = subprocess.run(["systemctl", "is-active", "--quiet", "nginx"])
|
|
437
|
+
if res.returncode == 0:
|
|
438
|
+
cmd = ["systemctl", "reload", "nginx"]
|
|
439
|
+
else:
|
|
440
|
+
cmd = ["systemctl", "start", "nginx"]
|
|
434
441
|
elif shutil.which("service"):
|
|
435
|
-
|
|
442
|
+
res = subprocess.run(["service", "nginx", "status"], capture_output=True)
|
|
443
|
+
if res.returncode == 0:
|
|
444
|
+
cmd = ["service", "nginx", "reload"]
|
|
445
|
+
else:
|
|
446
|
+
cmd = ["service", "nginx", "start"]
|
|
436
447
|
else:
|
|
437
448
|
nginx_bin = which_binary("nginx") or shutil.which("nginx")
|
|
438
449
|
if nginx_bin:
|
|
@@ -442,18 +453,88 @@ def reload_nginx(password: Optional[str] = None) -> bool:
|
|
|
442
453
|
return False
|
|
443
454
|
|
|
444
455
|
if os.geteuid() != 0:
|
|
445
|
-
if password:
|
|
446
|
-
validate_sudo_password(password)
|
|
447
456
|
cmd = ["sudo", "-n"] + cmd
|
|
448
457
|
|
|
449
458
|
try:
|
|
450
459
|
res = subprocess.run(cmd, capture_output=True, timeout=10)
|
|
451
460
|
return res.returncode == 0
|
|
452
461
|
except Exception as e:
|
|
453
|
-
logger.warning(f"Could not reload Nginx: {e}")
|
|
462
|
+
logger.warning(f"Could not reload/start Nginx: {e}")
|
|
454
463
|
return False
|
|
455
464
|
|
|
456
465
|
|
|
466
|
+
def install_nginx_ssl_cert(
|
|
467
|
+
domain: str,
|
|
468
|
+
src_cert: Path | str,
|
|
469
|
+
src_key: Path | str,
|
|
470
|
+
password: Optional[str] = None
|
|
471
|
+
) -> Tuple[Path, Path]:
|
|
472
|
+
"""
|
|
473
|
+
Install SSL certificate and key into /etc/nginx/ssl/{domain}.* with proper permissions and SELinux label.
|
|
474
|
+
Fixes 'BIO_new_file() failed (SSL: error:8000000d:system library:BIO_new_file:Permission denied)'
|
|
475
|
+
caused by systemd ProtectHome and SELinux restricting access to ~/.ndev/certs.
|
|
476
|
+
Returns (installed_cert_path, installed_key_path).
|
|
477
|
+
"""
|
|
478
|
+
ssl_dir = Path("/etc/nginx/ssl")
|
|
479
|
+
dest_cert = ssl_dir / f"{domain}.crt"
|
|
480
|
+
dest_key = ssl_dir / f"{domain}.key"
|
|
481
|
+
|
|
482
|
+
cert_content = Path(src_cert).read_text(encoding="utf-8")
|
|
483
|
+
key_content = Path(src_key).read_text(encoding="utf-8")
|
|
484
|
+
|
|
485
|
+
write_privileged_file(dest_cert, cert_content, password=password)
|
|
486
|
+
write_privileged_file(dest_key, key_content, password=password)
|
|
487
|
+
|
|
488
|
+
if os.geteuid() == 0:
|
|
489
|
+
try:
|
|
490
|
+
os.chmod(str(dest_cert), 0o644)
|
|
491
|
+
os.chmod(str(dest_key), 0o644)
|
|
492
|
+
except Exception:
|
|
493
|
+
pass
|
|
494
|
+
else:
|
|
495
|
+
try:
|
|
496
|
+
os.chmod(str(dest_cert), 0o644)
|
|
497
|
+
os.chmod(str(dest_key), 0o644)
|
|
498
|
+
except (PermissionError, OSError):
|
|
499
|
+
subprocess.run(["sudo", "-n", "chmod", "644", str(dest_cert), str(dest_key)], check=False)
|
|
500
|
+
|
|
501
|
+
if shutil.which("restorecon"):
|
|
502
|
+
cmd = ["restorecon", "-Rv", str(ssl_dir)]
|
|
503
|
+
if os.geteuid() != 0:
|
|
504
|
+
cmd = ["sudo", "-n"] + cmd
|
|
505
|
+
try:
|
|
506
|
+
subprocess.run(cmd, capture_output=True)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
return dest_cert, dest_key
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def ensure_docroot_accessible(root_path: Path) -> None:
|
|
514
|
+
"""
|
|
515
|
+
Ensure the parent directories up to root_path have execute permission (+x)
|
|
516
|
+
so the nginx worker process (e.g. user nginx) can traverse into document root.
|
|
517
|
+
"""
|
|
518
|
+
home = Path.home()
|
|
519
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
520
|
+
if sudo_user:
|
|
521
|
+
try:
|
|
522
|
+
import pwd
|
|
523
|
+
home = Path(pwd.getpwnam(sudo_user).pw_dir)
|
|
524
|
+
except Exception:
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
resolved_root = root_path.resolve()
|
|
529
|
+
resolved_home = home.resolve()
|
|
530
|
+
if resolved_root == resolved_home or resolved_root.is_relative_to(resolved_home):
|
|
531
|
+
st = home.stat()
|
|
532
|
+
if not (st.st_mode & 0o001):
|
|
533
|
+
os.chmod(str(home), st.st_mode | 0o001)
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
|
|
457
538
|
def list_vhosts() -> List[Dict[str, Any]]:
|
|
458
539
|
"""List all configured virtual hosts across all searched Nginx config directories."""
|
|
459
540
|
vhosts = []
|
|
@@ -513,8 +594,11 @@ def remove_vhost(domain: str, password: Optional[str] = None) -> bool:
|
|
|
513
594
|
except Exception as e:
|
|
514
595
|
logger.warning(f"Could not remove {conf_file}: {e}")
|
|
515
596
|
|
|
516
|
-
# Remove optional certificates
|
|
597
|
+
# Remove system and optional certificates
|
|
517
598
|
for cert_ext in [".crt", ".key"]:
|
|
599
|
+
sys_cert = Path(f"/etc/nginx/ssl/{clean_domain}{cert_ext}")
|
|
600
|
+
unlink_privileged(sys_cert, password=password)
|
|
601
|
+
|
|
518
602
|
cfile = NDEV_DIR / "certs" / f"{clean_domain}{cert_ext}"
|
|
519
603
|
if cfile.exists():
|
|
520
604
|
try:
|
|
@@ -361,7 +361,7 @@ class CreateVhostModal(ModalScreen[Optional[dict]]):
|
|
|
361
361
|
if not root and domain:
|
|
362
362
|
root = str(Path.home() / "Sites" / domain)
|
|
363
363
|
php_val = self.query_one("#select-vhost-php", Select).value
|
|
364
|
-
php_ver = str(php_val) if (php_val is not None and php_val != Select.BLANK) else self.default_php
|
|
364
|
+
php_ver = str(php_val) if (php_val is not None and php_val != Select.BLANK and php_val != getattr(Select, "NULL", None)) else self.default_php
|
|
365
365
|
ssl = self.query_one("#chk-vhost-ssl", Checkbox).value
|
|
366
366
|
|
|
367
367
|
if not domain:
|
|
@@ -474,7 +474,7 @@ class InstallPhpModal(ModalScreen[Optional[dict]]):
|
|
|
474
474
|
if event.button.id == "btn-modal-install":
|
|
475
475
|
custom = self.query_one("#input-custom-ver", Input).value.strip()
|
|
476
476
|
sel_val = self.query_one("#select-php-ver", Select).value
|
|
477
|
-
selected = str(sel_val) if (sel_val is not None and sel_val != Select.BLANK) else "8.4.25"
|
|
477
|
+
selected = str(sel_val) if (sel_val is not None and sel_val != Select.BLANK and sel_val != getattr(Select, "NULL", None)) else "8.4.25"
|
|
478
478
|
version = custom if custom else selected
|
|
479
479
|
|
|
480
480
|
self.dismiss({
|
|
@@ -1203,6 +1203,8 @@ class NdevDashboard(App):
|
|
|
1203
1203
|
get_nginx_vhost_dirs,
|
|
1204
1204
|
generate_vhost_config,
|
|
1205
1205
|
ensure_nginx_sites_enabled_included,
|
|
1206
|
+
install_nginx_ssl_cert,
|
|
1207
|
+
ensure_docroot_accessible,
|
|
1206
1208
|
write_privileged_file,
|
|
1207
1209
|
symlink_privileged,
|
|
1208
1210
|
add_to_hosts_privileged,
|
|
@@ -1220,6 +1222,18 @@ class NdevDashboard(App):
|
|
|
1220
1222
|
if ssl:
|
|
1221
1223
|
cert_path, key_path = generate_local_cert(domain, cert_dir)
|
|
1222
1224
|
|
|
1225
|
+
final_cert_path = cert_path
|
|
1226
|
+
final_key_path = key_path
|
|
1227
|
+
if ssl and cert_path and key_path:
|
|
1228
|
+
try:
|
|
1229
|
+
final_cert_path, final_key_path = install_nginx_ssl_cert(
|
|
1230
|
+
domain, cert_path, key_path, password=password
|
|
1231
|
+
)
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
logger.warning(f"Could not install SSL certificate into /etc/nginx/ssl: {e}")
|
|
1234
|
+
|
|
1235
|
+
ensure_docroot_accessible(Path(root))
|
|
1236
|
+
|
|
1223
1237
|
mm = php_ver.replace(".", "")[:2] if "." in php_ver else php_ver
|
|
1224
1238
|
selected_sock = NDEV_DIR / "run" / f"php{mm}.sock"
|
|
1225
1239
|
|
|
@@ -1228,8 +1242,8 @@ class NdevDashboard(App):
|
|
|
1228
1242
|
root=root,
|
|
1229
1243
|
selected_sock=selected_sock,
|
|
1230
1244
|
ssl=bool(ssl),
|
|
1231
|
-
cert_path=
|
|
1232
|
-
key_path=
|
|
1245
|
+
cert_path=final_cert_path,
|
|
1246
|
+
key_path=final_key_path,
|
|
1233
1247
|
)
|
|
1234
1248
|
write_privileged_file(conf_file, tpl, password=password)
|
|
1235
1249
|
if enabled_dir and enabled_dir != vhost_dir:
|
|
@@ -1430,19 +1444,22 @@ class NdevDashboard(App):
|
|
|
1430
1444
|
self.log_message(f"[bold red]✗ Failed to install {key}: {e}[/bold red]")
|
|
1431
1445
|
self.notify(f"Installation failed: {e}", severity="error")
|
|
1432
1446
|
|
|
1433
|
-
|
|
1434
|
-
async def on_select_changed(self, event: Select.Changed) -> None:
|
|
1447
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
1435
1448
|
"""Handle PHP version switcher or log selector dropdown changes."""
|
|
1436
1449
|
if self._updating_selects:
|
|
1437
1450
|
return
|
|
1438
1451
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1452
|
+
val = event.value
|
|
1453
|
+
if val in (Select.BLANK, getattr(Select, "NULL", None), None) or not isinstance(val, str):
|
|
1454
|
+
return
|
|
1455
|
+
|
|
1456
|
+
if event.select.id == "select-php-version":
|
|
1457
|
+
target_version = val.strip()
|
|
1441
1458
|
curr = CURRENT_LINK.resolve().name if (CURRENT_LINK.exists() or CURRENT_LINK.is_symlink()) else None
|
|
1442
|
-
if target_version != curr:
|
|
1443
|
-
|
|
1459
|
+
if target_version and target_version != curr and (PHP_DIR / target_version).exists():
|
|
1460
|
+
self._switch_php_version(target_version)
|
|
1444
1461
|
|
|
1445
|
-
elif event.select.id == "select-log-file"
|
|
1462
|
+
elif event.select.id == "select-log-file":
|
|
1446
1463
|
self._handle_tail_log()
|
|
1447
1464
|
|
|
1448
1465
|
def _handle_tail_log(self) -> None:
|
|
@@ -1450,7 +1467,7 @@ class NdevDashboard(App):
|
|
|
1450
1467
|
self.query_one(TabbedContent).active = "tab-logs"
|
|
1451
1468
|
log_select = self.query_one("#select-log-file", Select)
|
|
1452
1469
|
log_name = log_select.value
|
|
1453
|
-
if not log_name or log_name
|
|
1470
|
+
if not log_name or log_name in (Select.BLANK, getattr(Select, "NULL", None)):
|
|
1454
1471
|
if self._available_logs:
|
|
1455
1472
|
first_key = sorted(self._available_logs.keys())[0]
|
|
1456
1473
|
self._updating_selects = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndev-stack"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.12"
|
|
8
8
|
description = "Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -255,6 +255,27 @@ class TestNginxHelper(unittest.TestCase):
|
|
|
255
255
|
mock_run.return_value = unittest.mock.MagicMock(returncode=1)
|
|
256
256
|
self.assertTrue(nginx_helper.check_sudo_needs_password())
|
|
257
257
|
|
|
258
|
+
def test_install_nginx_ssl_cert(self):
|
|
259
|
+
src_cert = self.test_dir / "src.crt"
|
|
260
|
+
src_key = self.test_dir / "src.key"
|
|
261
|
+
src_cert.write_text("CERT_CONTENT")
|
|
262
|
+
src_key.write_text("KEY_CONTENT")
|
|
263
|
+
|
|
264
|
+
fake_ssl_dir = self.test_dir / "nginx_ssl"
|
|
265
|
+
with patch("ndev.linux.runtime.nginx_helper.Path") as mock_path:
|
|
266
|
+
def fake_p(p):
|
|
267
|
+
if str(p) == "/etc/nginx/ssl":
|
|
268
|
+
return fake_ssl_dir
|
|
269
|
+
return Path(p)
|
|
270
|
+
mock_path.side_effect = fake_p
|
|
271
|
+
|
|
272
|
+
with patch("os.geteuid", return_value=0):
|
|
273
|
+
dest_cert, dest_key = nginx_helper.install_nginx_ssl_cert("sample.local", src_cert, src_key)
|
|
274
|
+
self.assertTrue(dest_cert.exists())
|
|
275
|
+
self.assertTrue(dest_key.exists())
|
|
276
|
+
self.assertEqual(dest_cert.read_text(), "CERT_CONTENT")
|
|
277
|
+
self.assertEqual(dest_key.read_text(), "KEY_CONTENT")
|
|
278
|
+
|
|
258
279
|
def test_fastcgi_snippet_opensuse(self):
|
|
259
280
|
with patch.object(Path, "exists", autospec=True) as mock_exists:
|
|
260
281
|
def side_effect_exists(p):
|
|
@@ -443,5 +464,43 @@ class TestPhpDevelPackagesAndBuilder(unittest.TestCase):
|
|
|
443
464
|
self.assertEqual(resolve_installed_version("7.4"), "7.4")
|
|
444
465
|
|
|
445
466
|
|
|
467
|
+
class TestTuiSelectEventHandling(unittest.TestCase):
|
|
468
|
+
def test_on_select_changed_handles_null_and_blank(self):
|
|
469
|
+
from ndev.linux.tui import NdevDashboard
|
|
470
|
+
from textual.widgets import Select
|
|
471
|
+
|
|
472
|
+
app = NdevDashboard()
|
|
473
|
+
app._switch_php_version = MagicMock()
|
|
474
|
+
|
|
475
|
+
# Mock Select.Changed with Select.NULL
|
|
476
|
+
mock_event_null = MagicMock()
|
|
477
|
+
mock_event_null.select.id = "select-php-version"
|
|
478
|
+
mock_event_null.value = getattr(Select, "NULL", None)
|
|
479
|
+
|
|
480
|
+
app.on_select_changed(mock_event_null)
|
|
481
|
+
app._switch_php_version.assert_not_called()
|
|
482
|
+
|
|
483
|
+
# Mock Select.Changed with Select.BLANK
|
|
484
|
+
mock_event_blank = MagicMock()
|
|
485
|
+
mock_event_blank.select.id = "select-php-version"
|
|
486
|
+
mock_event_blank.value = Select.BLANK
|
|
487
|
+
|
|
488
|
+
app.on_select_changed(mock_event_blank)
|
|
489
|
+
app._switch_php_version.assert_not_called()
|
|
490
|
+
|
|
491
|
+
# Mock Select.Changed with valid version
|
|
492
|
+
mock_event_valid = MagicMock()
|
|
493
|
+
mock_event_valid.select.id = "select-php-version"
|
|
494
|
+
mock_event_valid.value = "8.4.27"
|
|
495
|
+
|
|
496
|
+
with patch("ndev.linux.tui.PHP_DIR") as mock_php_dir:
|
|
497
|
+
mock_php_dir.__truediv__.return_value.exists.return_value = True
|
|
498
|
+
with patch("ndev.linux.tui.CURRENT_LINK") as mock_link:
|
|
499
|
+
mock_link.resolve.return_value.name = "8.2.35"
|
|
500
|
+
mock_link.exists.return_value = True
|
|
501
|
+
app.on_select_changed(mock_event_valid)
|
|
502
|
+
app._switch_php_version.assert_called_once_with("8.4.27")
|
|
503
|
+
|
|
504
|
+
|
|
446
505
|
if __name__ == "__main__":
|
|
447
506
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|