Initial commit: 50 shell-script generators with web UI
- 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)
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user