部署脚本

This commit is contained in:
2026-06-04 16:29:52 +08:00
parent 447cacc4f6
commit 25decaba71
5 changed files with 210 additions and 17 deletions
+20 -1
View File
@@ -54,6 +54,7 @@ type webConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
SocketPath string `yaml:"socket_path"`
StaticDir string `yaml:"static_dir"`
Admin webAdminConfig `yaml:"admin"`
}
@@ -106,6 +107,7 @@ type rawWebConfig struct {
Enabled *bool `yaml:"enabled"`
Host *string `yaml:"host"`
Port *int `yaml:"port"`
SocketPath *string `yaml:"socket_path"`
StaticDir *string `yaml:"static_dir"`
Admin *rawWebAdminConfig `yaml:"admin"`
}
@@ -141,6 +143,7 @@ func defaultConfig() *config {
Enabled: true,
Host: "0.0.0.0",
Port: 8080,
SocketPath: defaultWebSocketPath(),
StaticDir: "./dist",
Admin: webAdminConfig{
Username: "admin",
@@ -169,6 +172,17 @@ func defaultSQLitePath() string {
return defaultSQLitePathForGOOS(runtime.GOOS)
}
func defaultWebSocketPath() string {
return defaultWebSocketPathForGOOS(runtime.GOOS)
}
func defaultWebSocketPathForGOOS(goos string) string {
if goos == "windows" {
return filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
}
return filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
}
func defaultSQLitePathForGOOS(goos string) string {
if goos == "windows" {
return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
@@ -307,6 +321,11 @@ func normalizeConfig(raw rawConfig) (*config, bool) {
} else {
cfg.Web.Port = *raw.Web.Port
}
if raw.Web.SocketPath == nil {
changed = true
} else {
cfg.Web.SocketPath = *raw.Web.SocketPath
}
if raw.Web.StaticDir == nil {
changed = true
} else {
@@ -358,7 +377,7 @@ func validateConfig(cfg *config) error {
return fmt.Errorf("invalid database.driver %q: must be sqlite or mysql", cfg.Database.Driver)
}
if cfg.Web.Enabled {
if cfg.Web.Port <= 0 || cfg.Web.Port > 65535 {
if cfg.Web.SocketPath == "" && (cfg.Web.Port <= 0 || cfg.Web.Port > 65535) {
return fmt.Errorf("invalid web port %d: must be 1-65535", cfg.Web.Port)
}
if cfg.Web.StaticDir == "" {
+18 -1
View File
@@ -38,6 +38,9 @@ func TestLoadConfigCreatesDefaultFile(t *testing.T) {
if cfg.Web.Port != 8080 {
t.Fatalf("web port = %d, want 8080", cfg.Web.Port)
}
if cfg.Web.SocketPath != defaultWebSocketPath() {
t.Fatalf("web socket path = %q, want %q", cfg.Web.SocketPath, defaultWebSocketPath())
}
if cfg.Web.StaticDir != "./dist" {
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
}
@@ -77,7 +80,7 @@ func TestLoadConfigFillsMissingFields(t *testing.T) {
t.Fatal(err)
}
text := string(data)
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "static_dir:"} {
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "socket_path:", "static_dir:"} {
if !strings.Contains(text, want) {
t.Fatalf("completed config missing %q in:\n%s", want, text)
}
@@ -187,11 +190,25 @@ func TestValidateConfigDatabase(t *testing.T) {
func TestValidateConfigWeb(t *testing.T) {
cfg := defaultConfig()
cfg.Web.SocketPath = ""
cfg.Web.Port = 0
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
t.Fatalf("invalid web port error = %v, want web port error", err)
}
cfg = defaultConfig()
cfg.Web.Port = 0
if err := validateConfig(cfg); err != nil {
t.Fatalf("web socket with invalid port error = %v, want nil", err)
}
cfg = defaultConfig()
cfg.Web.SocketPath = ""
cfg.Web.Port = 0
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
t.Fatalf("invalid web port without socket error = %v, want web port error", err)
}
cfg = defaultConfig()
cfg.Web.StaticDir = ""
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") {
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="mesh_mqtt_go"
SERVICE_USER="mesh_mqtt_go"
CONFIG_DIR="/etc/${SERVICE_NAME}"
DATA_DIR="/srv/${SERVICE_NAME}"
INSTALL_DIR="/opt/${SERVICE_NAME}"
SOCKET_PATH="${INSTALL_DIR}/web.sock"
FRONTEND_DIR="meshmap_frontend"
BINARY_NAME="${SERVICE_NAME}"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [[ "${EUID}" -ne 0 ]]; then
echo "请使用 root 权限运行: sudo $0" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${SCRIPT_DIR}"
echo "拉取最新代码..."
git pull
echo "编译前端..."
cd "${SCRIPT_DIR}/${FRONTEND_DIR}"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
echo "编译 Go 程序..."
cd "${SCRIPT_DIR}"
go build -o "${BINARY_NAME}" .
echo "检查系统用户..."
if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then
useradd --system --home-dir "${DATA_DIR}" --shell /usr/sbin/nologin "${SERVICE_USER}"
fi
echo "创建目录..."
install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${CONFIG_DIR}" "${DATA_DIR}"
install -d -m 0755 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${INSTALL_DIR}"
echo "安装程序和前端文件..."
install -m 0755 -o root -g root "${SCRIPT_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
rm -rf "${INSTALL_DIR}/dist"
cp -a "${SCRIPT_DIR}/${FRONTEND_DIR}/dist" "${INSTALL_DIR}/dist"
chown root:root "${INSTALL_DIR}/${BINARY_NAME}"
chown -R root:root "${INSTALL_DIR}/dist"
chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}"
chmod 0755 "${INSTALL_DIR}"
find "${INSTALL_DIR}/dist" -type d -exec chmod 0755 {} \;
find "${INSTALL_DIR}/dist" -type f -exec chmod 0644 {} \;
if [[ ! -f "${CONFIG_DIR}/config.yaml" ]]; then
cat > "${CONFIG_DIR}/config.yaml" <<EOF
mqtt:
host: 0.0.0.0
port: 1883
tls:
enabled: false
cert_file: ""
key_file: ""
meshtastic:
psk: AQ==
database:
driver: sqlite
sqlite:
path: ${DATA_DIR}/${SERVICE_NAME}.db
mysql:
dsn: ""
web:
enabled: true
host: 0.0.0.0
port: 8080
socket_path: ${SOCKET_PATH}
static_dir: ${INSTALL_DIR}/dist
admin:
username: admin
password: admin
session_secret: ""
session_secure: false
EOF
chown "${SERVICE_USER}:${SERVICE_USER}" "${CONFIG_DIR}/config.yaml"
chmod 0640 "${CONFIG_DIR}/config.yaml"
fi
echo "写入 systemd 服务文件..."
cat > "${SERVICE_FILE}" <<EOF
[Unit]
Description=Mesh MQTT Go Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_USER}
WorkingDirectory=${INSTALL_DIR}
ExecStart=${INSTALL_DIR}/${BINARY_NAME} -web-socket-path ${SOCKET_PATH} -web-static-dir ${INSTALL_DIR}/dist
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=${CONFIG_DIR} ${DATA_DIR} ${INSTALL_DIR}
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
systemctl restart "${SERVICE_NAME}"
echo "部署完成,服务状态:"
systemctl --no-pager --full status "${SERVICE_NAME}"
+12 -1
View File
@@ -214,6 +214,7 @@ func parseArgs() (*config, error) {
flag.BoolVar(&cfg.Web.Enabled, "web", cfg.Web.Enabled, "Enable Gin web server")
flag.StringVar(&cfg.Web.Host, "web-host", cfg.Web.Host, "Web server listen host")
flag.IntVar(&cfg.Web.Port, "web-port", cfg.Web.Port, "Web server listen port")
flag.StringVar(&cfg.Web.SocketPath, "web-socket-path", cfg.Web.SocketPath, "Web server Unix socket path; empty uses host and port")
flag.StringVar(&cfg.Web.StaticDir, "web-static-dir", cfg.Web.StaticDir, "Web frontend static files directory")
flag.StringVar(&cfg.Web.Admin.Username, "admin-username", cfg.Web.Admin.Username, "Web admin username")
flag.Parse()
@@ -267,12 +268,22 @@ func run(cfg *config) error {
}
mqttStatus := mqttRuntimeStatus{server: server, address: mqttAddr, tls: cfg.MQTT.TLS.Enabled, stats: messageStats}
httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking)
webAddress := httpServer.Addr
go func() {
if cfg.Web.SocketPath != "" {
if err := serveHTTPUnixSocket(httpServer, cfg.Web.SocketPath); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
return
}
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
printJSON(map[string]any{"event": "web_started", "address": httpServer.Addr, "static_dir": cfg.Web.StaticDir})
if cfg.Web.SocketPath != "" {
webAddress = cfg.Web.SocketPath
}
printJSON(map[string]any{"event": "web_started", "address": webAddress, "static_dir": cfg.Web.StaticDir})
}
sigCh := make(chan os.Signal, 1)
+26
View File
@@ -21,6 +21,32 @@ func newHTTPServer(cfg webConfig, store *store, sessions *sessionManager, mqttSt
}
}
func serveHTTPUnixSocket(server *http.Server, socketPath string) error {
if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
return err
}
if info, err := os.Stat(socketPath); err == nil {
if info.Mode()&os.ModeSocket == 0 {
return errors.New("web socket path exists and is not a socket")
}
if err := os.Remove(socketPath); err != nil {
return err
}
} else if !os.IsNotExist(err) {
return err
}
listener, err := net.Listen("unix", socketPath)
if err != nil {
return err
}
defer os.Remove(socketPath)
if err := os.Chmod(socketPath, 0660); err != nil {
listener.Close()
return err
}
return server.Serve(listener)
}
func newRouter(cfg webConfig, store *store, sessions *sessionManager, mqttStatus mqttStatusProvider, blocking *blockingCache) *gin.Engine {
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())