---
name: startup-service-manager
description: "Generic Linux service manager script — start/stop/restart/status any service via simple .sh definitions. No systemd required."
tags: [devops, services, startup, process-management, linux]
---

# Startup - Generic Service Manager

A lightweight bash-based service manager at `~/startup/`. Manages any process (mihomo, yacd, custom apps) via simple `.sh` definition files. No systemd, no Docker dependency.

## When to Use

- User wants to manage multiple background services from one place
- Services don't have their own systemd units
- Quick prototyping before committing to systemd
- User explicitly asks for a startup/service management script

## Architecture

```
~/startup/
├── startup.sh              # Main script
├── startup                 # Symlink/wrapper for convenience
├── services/               # One .sh per service (the registry)
│   ├── mihomo.sh
│   └── yacd.sh
├── pids/                   # Auto-managed PID files
└── logs/                   # Per-service stdout/stderr logs
```

## Commands

```bash
bash ~/startup/startup.sh status              # Show all services (running/stopped + PID)
bash ~/startup/startup.sh list                # List all registered services (name + port + desc)
bash ~/startup/startup.sh start               # Start all
bash ~/startup/startup.sh stop                # Stop all
bash ~/startup/startup.sh restart mihomo      # Restart one
bash ~/startup/startup.sh log mihomo          # View logs
bash ~/startup/startup.sh register nginx      # Create template
bash ~/startup/startup.sh remove nginx        # Delete service
bash ~/startup/startup.sh help                # Show help
```

`status` vs `list`: `status` shows runtime state (PID, running/stopped), `list` shows a clean registry view (name, port, description).

If alias is set (`alias startup="bash ~/startup/startup.sh"`), just type `startup status`.

## Service Definition Format

Each service is a `.sh` file in `~/startup/services/`. Example (`mihomo.sh`):

```bash
SVC_DESC="Clash/Mihomo 代理引擎"
SVC_PORT="7890 (代理) / 9090 (API)"
SVC_PID_PATTERN="mihomo -d"
SVC_START="cd ~/clash && exec ./mihomo -d config"
SVC_HEALTH="curl -s http://127.0.0.1:9090/version"
```

**Required:** `SVC_START` — the command to launch the service (must background itself or use `exec` with the script's `&`).

**Optional:**
| Variable | Purpose |
|----------|---------|
| `SVC_DESC` | Human-readable description (shown in `status`) |
| `SVC_PORT` | Port info string (display only) |
| `SVC_PID_PATTERN` | `pkill -f` pattern for cleanup of stale processes |
| `SVC_HEALTH` | Command that exits 0 when service is ready (used for startup wait) |
| `SVC_STOP` | Custom stop command (before SIGTERM) |

## Registering a New Service

```bash
# Method 1: Use register command (creates template)
bash ~/startup/startup.sh register myapp
# Then edit ~/startup/services/myapp.sh

# Method 2: Write directly
cat > ~/startup/services/myapp.sh << 'EOF'
SVC_DESC="My Application"
SVC_PORT="8080"
SVC_PID_PATTERN="myapp"
SVC_START="cd ~/myapp && exec node server.js"
SVC_HEALTH="curl -s http://127.0.0.1:8080/health"
EOF
```

## Quick-Start Service Definitions

Ready-to-use definitions for common services: `skill_view(name="startup-service-manager", file_path="references/common-services.md")`

## Auto-Start with systemd

To make startup services auto-start on boot, create a systemd unit:

```bash
cat > /tmp/startup-services.service << 'EOF'
[Unit]
Description=Startup Services (mihomo, yacd, hermes-dashboard)
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
User=openclaw
ExecStart=/home/openclaw/startup/startup.sh start all
ExecStop=/home/openclaw/startup/startup.sh stop all

[Install]
WantedBy=multi-user.target
EOF

sudo cp /tmp/startup-services.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable startup-services.service
```

**Important:** `startup.sh` must have execute permission: `chmod +x ~/startup/startup.sh`

## Pitfalls

- **PID file stale after crash**: If a service crashes without cleanup, the PID file remains. The script checks `kill -0` so it detects dead PIDs, but `start` will refuse. Fix: `rm ~/startup/pids/<name>.pid` then start again, or just `startup restart <name>` which stops (cleans up) then starts.
- **systemd permission denied**: If systemd fails with "Permission denied" when executing startup.sh, run `chmod +x ~/startup/startup.sh`. The script needs execute permission for systemd to run it.
- **Multiple processes on same pattern**: `SVC_PID_PATTERN` is used with `pkill -f`, which kills ALL matching processes. If the pattern is too broad (e.g., just `python`), it will kill unrelated processes. Use specific patterns like `http.server 9091`.
- **Health check timeout**: Default wait is 10 seconds. If your service takes longer to start, the script still succeeds (PID is alive) but logs "启动成功" before health check passes. For slow-starting services, increase the loop or accept the delay.
- **Log rotation**: Logs append (`>>`), never rotate. For long-running production, either add logrotate or periodically truncate: `> ~/startup/logs/mihomo.log`.
- **startup.sh vs systemd**: Don't use both for the same service — they'll fight over PIDs. Pick one management system per service.
- **⚠️ CRITICAL: systemd cgroup kills child processes on exit**: If `startup.sh restart <service>` is called from inside a systemd oneshot service (e.g., a watchdog timer), the spawned process inherits the service's cgroup. When the oneshot service exits, **systemd kills all processes in the cgroup — including the just-started service**. This creates a death loop: watchdog starts service → watchdog exits → systemd kills service → next watchdog cycle finds service dead → repeat. **Fix**: Services managed by a systemd-timer watchdog MUST run as their own dedicated systemd service. The watchdog should call `systemctl --user restart <service>` instead of `startup.sh restart <service>`. See `skill_view(name="clash-docker-workflow", file_path="references/mihomo-watchdog.md")` for the full diagnosis and fix.
- **Alias not available in new terminals**: After adding `alias startup=...` to `.bashrc`, existing terminals need `source ~/.bashrc`. New terminals get it automatically.
- **Tracking existing processes**: When a service was started outside startup.sh (manually, or by another system), the PID file won't exist. The script will show "已停止" even though the process is running. Fix: `echo $(pgrep -f "pattern") > ~/startup/pids/<name>.pid`. Or restart through startup.sh to take over management.
- **Duplicate instances**: If you manually start a service that's already running via startup.sh, you get two processes competing for the same port. Always check `startup status` before starting. The `stop` command uses both PID file AND `pkill -f SVC_PID_PATTERN` to catch stragglers.
- **SVC_HEALTH with curl -w format**: In `eval` context, `curl -w '%{http_code}'` can misbehave because `%` and `{}` are interpreted by the shell. Use double quotes inside: `curl -s ... -o /dev/null -w '"'"'%{http_code}'"'"'` or test with a simpler check like `ss -tlnp | grep -q PORT`.
- **Alias in .bashrc**: When adding `alias startup=...` to `.bashrc`, the line `grep -q || echo >> ~/.bashrc` pattern needs the user's approval for writing to .bashrc. Use `write_file` or ask user consent.
- **Health watchdog vs service manager**: startup.sh manages start/stop lifecycle but doesn't monitor ongoing health. For services that need continuous health monitoring (e.g., mihomo proxy whose crash cascades to Telegram adapter), use a watchdog pattern — see `skill_view(name="clash-docker-workflow", file_path="references/mihomo-watchdog.md")`.
