/supervisor.py

# imports - standard imports
import getpass
import logging
import os

# imports - third party imports
import click

# imports - module imports
import bench
from bench.app import use_rq
from bench.bench import Bench
from bench.config.common_site_config import (
	compute_max_requests_jitter,
	get_config,
	get_default_max_requests,
	get_gunicorn_workers,
	update_config,
)
from bench.utils import get_bench_name, which

logger = logging.getLogger(bench.PROJECT_NAME)


def generate_supervisor_config(bench_path, user=None, yes=False, skip_redis=False):
	"""Generate supervisor config for respective bench path"""
	if not user:
		user = getpass.getuser()

	config = Bench(bench_path).conf
	template = bench.config.env().get_template("supervisor.conf")
	bench_dir = os.path.abspath(bench_path)

	web_worker_count = config.get(
		"gunicorn_workers", get_gunicorn_workers()["gunicorn_workers"]
	)
	max_requests = config.get(
		"gunicorn_max_requests", get_default_max_requests(web_worker_count)
	)

	config = template.render(
		**{
			"bench_dir": bench_dir,
			"sites_dir": os.path.join(bench_dir, "sites"),
			"user": user,
			"use_rq": use_rq(bench_path),
			"http_timeout": config.get("http_timeout", 120),
			"redis_server": which("redis-server"),
			"node": which("node") or which("nodejs"),
			"redis_cache_config": os.path.join(bench_dir, "config", "redis_cache.conf"),
			"redis_queue_config": os.path.join(bench_dir, "config", "redis_queue.conf"),
			"webserver_port": config.get("webserver_port", 8000),
			"gunicorn_workers": web_worker_count,
			"gunicorn_max_requests": max_requests,
			"gunicorn_max_requests_jitter": compute_max_requests_jitter(max_requests),
			"bench_name": get_bench_name(bench_path),
			"background_workers": config.get("background_workers") or 1,
			"bench_cmd": which("bench"),
			"skip_redis": skip_redis,
			"workers": config.get("workers", {}),
			"multi_queue_consumption": can_enable_multi_queue_consumption(bench_path),
			"supervisor_startretries": 10,
		}
	)

	conf_path = os.path.join(bench_path, "config", "supervisor.conf")
	if not yes and os.path.exists(conf_path):
		click.confirm(
			"supervisor.conf already exists and this will overwrite it. Do you want to continue?",
			abort=True,
		)

	with open(conf_path, "w") as f:
		f.write(config)

	update_config({"restart_supervisor_on_update": True}, bench_path=bench_path)
	update_config({"restart_systemd_on_update": False}, bench_path=bench_path)
	sync_socketio_port(bench_path)


def get_supervisord_conf():
	"""Returns path of supervisord config from possible paths"""
	possibilities = (
		"supervisord.conf",
		"etc/supervisord.conf",
		"/etc/supervisord.conf",
		"/etc/supervisor/supervisord.conf",
		"/etc/supervisord.conf",
	)

	for possibility in possibilities:
		if os.path.exists(possibility):
			return possibility


def sync_socketio_port(bench_path):
	# Backward compatbility: always keep redis_cache and redis_socketio port same
	common_config = get_config(bench_path=bench_path)

	socketio_port = common_config.get("redis_socketio")
	cache_port = common_config.get("redis_cache")
	if socketio_port and socketio_port != cache_port:
		update_config({"redis_socketio": cache_port})


def can_enable_multi_queue_consumption(bench_path: str) -> bool:
	try:
		from semantic_version import Version

		from bench.utils.app import get_current_version

		supported_version = Version(major=14, minor=18, patch=0)

		frappe_version = Version(get_current_version("frappe", bench_path=bench_path))

		return frappe_version > supported_version
	except Exception:
		return False


def check_supervisord_config(user=None):
	"""From bench v5.x, we're moving to supervisor running as user"""
	# i don't think bench should be responsible for this but we're way past this now...
	# removed updating supervisord conf & reload in Aug 2022 - gavin@frappe.io
	import configparser

	if not user:
		user = getpass.getuser()

	supervisord_conf = get_supervisord_conf()
	section = "unix_http_server"
	updated_values = {"chmod": "0760", "chown": f"{user}:{user}"}
	supervisord_conf_changes = ""

	if not supervisord_conf:
		logger.log("supervisord.conf not found")
		return

	config = configparser.ConfigParser()
	config.read(supervisord_conf)

	if section not in config.sections():
		config.add_section(section)
		action = f"Section {section} Added"
		logger.log(action)
		supervisord_conf_changes += "
" + action

	for key, value in updated_values.items():
		try:
			current_value = config.get(section, key)
		except configparser.NoOptionError:
			current_value = ""

		if current_value.strip() != value:
			config.set(section, key, value)
			action = (
				f"Updated supervisord.conf: '{key}' changed from '{current_value}' to '{value}'"
			)
			logger.log(action)
			supervisord_conf_changes += "
" + action

	if not supervisord_conf_changes:
		logger.error("supervisord.conf not updated")
		contents = "
".join(f"{x}={y}" for x, y in updated_values.items())
		print(
			f"Update your {supervisord_conf} with the following values:
[{section}]
{contents}"
		)