319f683a16
- 8 categories / 50 generators covering middleware, databases, runtimes, systemd services, system tools, network, monitoring, security - VNC supports XFCE / GNOME / KDE Plasma / MATE / LXQt desktops - Node.js versions 18-26 (incl. current LTS 24 Krypton and current 26) - Live preview, copy / download / multi-script bundle in web UI - Distro-aware (Ubuntu/Debian/CentOS/RHEL/Rocky/Alma/Fedora) - All 50 generators pass bash -n syntax check - Zero pip dependencies (Flask stdlib only)
235 lines
9.4 KiB
Python
235 lines
9.4 KiB
Python
"""
|
|
Systemd service generator — a single, generic Generator that produces a fully-configured
|
|
systemd unit for arbitrary user-provided commands. Plus presets for common cases.
|
|
"""
|
|
from . import register, Generator, Field
|
|
from . import bash_header, quote, bool_str, yes
|
|
|
|
|
|
class SystemdGeneric(Generator):
|
|
id = "systemd-generic"
|
|
title = "通用 systemd 服务"
|
|
category = "systemd"
|
|
icon = "🛠️"
|
|
tags = ["systemd", "service", "unit"]
|
|
description = "为任意可执行命令生成 systemd unit 文件,支持开机自启、自动重启、日志轮转。"
|
|
fields = [
|
|
Field("service_name", "服务名", "text", default="myapp",
|
|
help="将创建 /etc/systemd/system/<name>.service"),
|
|
Field("description", "Description", "text", default="My custom app"),
|
|
Field("exec_start", "ExecStart 命令", "textarea", required=True,
|
|
default="/opt/myapp/bin/server --config /etc/myapp/server.conf",
|
|
help="要执行的命令,可以有多行(用 ; 串接)或用绝对路径。"),
|
|
Field("exec_stop", "ExecStop 命令", "text", default="",
|
|
help="留空则发 SIGTERM;填入完整命令。"),
|
|
Field("exec_reload", "ExecReload 命令", "text", default="",
|
|
help="通常是 'kill -HUP $MAINPID' 或 nginx -s reload。"),
|
|
Field("type", "Type", "select", default="simple",
|
|
options=["simple", "forking", "oneshot", "notify", "exec"]),
|
|
Field("user", "运行用户", "text", default="root"),
|
|
Field("group", "运行组", "text", default="root"),
|
|
Field("working_dir", "WorkingDirectory", "text", default="/"),
|
|
Field("env_vars", "环境变量", "textarea", default="NODE_ENV=production\nLOG_LEVEL=info",
|
|
help="每行 KEY=VALUE。"),
|
|
Field("restart", "Restart 策略", "select", default="on-failure",
|
|
options=["no", "always", "on-success", "on-failure", "on-abnormal",
|
|
"on-abort", "on-watchdog"]),
|
|
Field("restart_sec", "RestartSec (秒)", "number", default="5"),
|
|
Field("limit_nofile", "LimitNOFILE", "number", default="65536"),
|
|
Field("limit_nproc", "LimitNPROC", "number", default="65536"),
|
|
Field("kill_mode", "KillMode", "select", default="mixed",
|
|
options=["control-group", "process", "mixed", "none"]),
|
|
Field("enable_after_network", "After=network.target", "checkbox", default="yes"),
|
|
Field("enable_log_dir", "创建日志目录 /var/log/<name>", "checkbox", default="yes"),
|
|
Field("auto_start", "脚本执行完自动 enable+start", "checkbox", default="yes"),
|
|
]
|
|
|
|
def render(self, p):
|
|
name = p.get("service_name", "myapp").replace(" ", "-")
|
|
desc = p.get("description", "My custom app")
|
|
exec_start = p.get("exec_start", "").strip()
|
|
if not exec_start:
|
|
raise ValueError("exec_start 不能为空")
|
|
exec_stop = p.get("exec_stop", "").strip()
|
|
exec_reload = p.get("exec_reload", "").strip()
|
|
type_ = p.get("type", "simple")
|
|
user = p.get("user", "root")
|
|
group = p.get("group", "root")
|
|
wd = p.get("working_dir", "/")
|
|
envs = [e.strip() for e in (p.get("env_vars", "") or "").splitlines() if e.strip() and "=" in e]
|
|
restart = p.get("restart", "on-failure")
|
|
restart_sec = str(p.get("restart_sec", "5"))
|
|
nofile = str(p.get("limit_nofile", "65536"))
|
|
nproc = str(p.get("limit_nproc", "65536"))
|
|
kill = p.get("kill_mode", "mixed")
|
|
after_nw = bool_str(p.get("enable_after_network", True))
|
|
log_dir = bool_str(p.get("enable_log_dir", True))
|
|
auto = bool_str(p.get("auto_start", True))
|
|
|
|
out = [bash_header(self.title)]
|
|
out.append('log "Writing unit file /etc/systemd/system/' + name + '.service"')
|
|
out.append('mkdir -p /etc/systemd/system')
|
|
if log_dir:
|
|
out.append('mkdir -p /var/log/' + name)
|
|
unit = ['[Unit]']
|
|
unit.append('Description=' + desc)
|
|
if after_nw:
|
|
unit.append('After=network.target network-online.target')
|
|
unit.append('Wants=network-online.target')
|
|
unit.append('')
|
|
unit.append('[Service]')
|
|
unit.append('Type=' + type_)
|
|
unit.append('User=' + user)
|
|
unit.append('Group=' + group)
|
|
unit.append('WorkingDirectory=' + wd)
|
|
for env in envs:
|
|
unit.append('Environment="' + env.replace('"', '\\"') + '"')
|
|
unit.append('ExecStart=' + exec_start.replace('\n', ' && '))
|
|
if exec_stop:
|
|
unit.append('ExecStop=' + exec_stop)
|
|
if exec_reload:
|
|
unit.append('ExecReload=' + exec_reload)
|
|
unit.append('Restart=' + restart)
|
|
unit.append('RestartSec=' + restart_sec)
|
|
unit.append('LimitNOFILE=' + nofile)
|
|
unit.append('LimitNPROC=' + nproc)
|
|
unit.append('KillMode=' + kill)
|
|
unit.append('StandardOutput=journal')
|
|
unit.append('StandardError=journal')
|
|
if log_dir:
|
|
unit.append('StandardOutput=append:/var/log/' + name + '/stdout.log')
|
|
unit.append('StandardError=append:/var/log/' + name + '/stderr.log')
|
|
unit.append('')
|
|
unit.append('[Install]')
|
|
unit.append('WantedBy=multi-user.target')
|
|
out.append('cat > /etc/systemd/system/' + name + '.service <<UNIT_EOF\n' + '\n'.join(unit) + '\nUNIT_EOF')
|
|
out.append('systemctl daemon-reload')
|
|
if auto:
|
|
out.append('systemctl enable --now ' + name)
|
|
out.append('sleep 1')
|
|
out.append('systemctl --no-pager status ' + name + ' || true')
|
|
else:
|
|
out.append('log "Unit installed but not started. Run: systemctl enable --now ' + name + '"')
|
|
out.append('log "Useful commands:')
|
|
out.append(' systemctl status ' + name)
|
|
out.append(' journalctl -u ' + name + ' -f')
|
|
out.append(' systemctl restart ' + name + '"')
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
# A few presets that wrap the same engine with sensible defaults
|
|
def _make_preset(id_, title, icon, description, defaults, tags=()):
|
|
class G(SystemdGeneric):
|
|
pass
|
|
G.id = id_
|
|
G.title = title
|
|
G.icon = icon
|
|
G.description = description
|
|
G.tags = list(tags)
|
|
G.fields = [] # CRITICAL: avoid mutating the parent's shared list
|
|
for f in SystemdGeneric.fields:
|
|
f2 = Field(f.name, f.label, f.type, f.default, f.placeholder, f.help,
|
|
f.options, f.required, f.min, f.max, f.step, f.pattern, f.group)
|
|
if f.name in defaults:
|
|
f2.default = defaults[f.name]
|
|
G.fields.append(f2)
|
|
return G
|
|
|
|
|
|
# preset: Python uvicorn/gunicorn web app
|
|
GunicornSvc = _make_preset(
|
|
id_="svc-gunicorn",
|
|
title="Gunicorn (Python) systemd 服务",
|
|
icon="🐍",
|
|
description="为 gunicorn 应用生成 systemd unit,自动设置 GUNICORN_CMD_ARGS、日志目录。",
|
|
defaults={
|
|
"service_name": "myapp",
|
|
"description": "My Python web app (gunicorn)",
|
|
"exec_start": "/opt/myapp/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 app:app",
|
|
"type": "simple",
|
|
"user": "app",
|
|
"working_dir": "/opt/myapp",
|
|
"env_vars": "PYTHONUNBUFFERED=1\nLOG_LEVEL=info",
|
|
},
|
|
tags=("python", "gunicorn", "flask", "django", "fastapi"),
|
|
)
|
|
register(GunicornSvc())
|
|
|
|
|
|
# preset: Node.js pm2-style systemd
|
|
NodeSvc = _make_preset(
|
|
id_="svc-nodejs",
|
|
title="Node.js (npm start) systemd 服务",
|
|
icon="🟢",
|
|
description="为 Node.js 应用生成 systemd unit,支持 npm start / node 直接启动。",
|
|
defaults={
|
|
"service_name": "nodeapp",
|
|
"description": "Node.js application",
|
|
"exec_start": "/usr/bin/node /opt/nodeapp/server.js",
|
|
"user": "app",
|
|
"working_dir": "/opt/nodeapp",
|
|
"env_vars": "NODE_ENV=production\nPORT=3000",
|
|
},
|
|
tags=("nodejs", "node", "express", "next"),
|
|
)
|
|
register(NodeSvc())
|
|
|
|
|
|
# preset: Java -jar
|
|
JavaSvc = _make_preset(
|
|
id_="svc-java",
|
|
title="Java -jar systemd 服务",
|
|
icon="☕",
|
|
description="为 SpringBoot/任意 Java -jar 应用生成 systemd unit,自动设置 JAVA_HOME。",
|
|
defaults={
|
|
"service_name": "javaapp",
|
|
"description": "Java application",
|
|
"exec_start": "/usr/bin/java -Xms512m -Xmx1024m -jar /opt/javaapp/app.jar --server.port=8080",
|
|
"user": "app",
|
|
"working_dir": "/opt/javaapp",
|
|
"env_vars": "JAVA_HOME=/usr/lib/jvm/java-17-openjdk",
|
|
},
|
|
tags=("java", "springboot", "jar"),
|
|
)
|
|
register(JavaSvc())
|
|
|
|
|
|
# preset: Go binary
|
|
GoSvc = _make_preset(
|
|
id_="svc-go",
|
|
title="Go binary systemd 服务",
|
|
icon="🐹",
|
|
description="为编译后的 Go 二进制生成 systemd unit。",
|
|
defaults={
|
|
"service_name": "goapp",
|
|
"description": "Go application",
|
|
"exec_start": "/opt/goapp/bin/server -config /etc/goapp/server.yaml",
|
|
"user": "app",
|
|
"working_dir": "/opt/goapp",
|
|
},
|
|
tags=("go", "golang"),
|
|
)
|
|
register(GoSvc())
|
|
|
|
|
|
# preset: shell script
|
|
ShellSvc = _make_preset(
|
|
id_="svc-shell",
|
|
title="Shell 脚本 systemd 服务",
|
|
icon="📜",
|
|
description="为任意 shell 脚本生成 systemd unit,适合定时/常驻脚本。",
|
|
defaults={
|
|
"service_name": "myscript",
|
|
"description": "Custom shell script service",
|
|
"exec_start": "/opt/myscript/run.sh",
|
|
"user": "root",
|
|
"working_dir": "/opt/myscript",
|
|
},
|
|
tags=("bash", "script"),
|
|
)
|
|
register(ShellSvc())
|
|
|
|
|
|
# Register the generic one as well
|
|
register(SystemdGeneric())
|