部署脚本
This commit is contained in:
@@ -54,6 +54,7 @@ type webConfig struct {
|
|||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
|
SocketPath string `yaml:"socket_path"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
Admin webAdminConfig `yaml:"admin"`
|
Admin webAdminConfig `yaml:"admin"`
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ type rawWebConfig struct {
|
|||||||
Enabled *bool `yaml:"enabled"`
|
Enabled *bool `yaml:"enabled"`
|
||||||
Host *string `yaml:"host"`
|
Host *string `yaml:"host"`
|
||||||
Port *int `yaml:"port"`
|
Port *int `yaml:"port"`
|
||||||
|
SocketPath *string `yaml:"socket_path"`
|
||||||
StaticDir *string `yaml:"static_dir"`
|
StaticDir *string `yaml:"static_dir"`
|
||||||
Admin *rawWebAdminConfig `yaml:"admin"`
|
Admin *rawWebAdminConfig `yaml:"admin"`
|
||||||
}
|
}
|
||||||
@@ -141,6 +143,7 @@ func defaultConfig() *config {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
|
SocketPath: defaultWebSocketPath(),
|
||||||
StaticDir: "./dist",
|
StaticDir: "./dist",
|
||||||
Admin: webAdminConfig{
|
Admin: webAdminConfig{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
@@ -169,6 +172,17 @@ func defaultSQLitePath() string {
|
|||||||
return defaultSQLitePathForGOOS(runtime.GOOS)
|
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 {
|
func defaultSQLitePathForGOOS(goos string) string {
|
||||||
if goos == "windows" {
|
if goos == "windows" {
|
||||||
return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
|
return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
|
||||||
@@ -307,6 +321,11 @@ func normalizeConfig(raw rawConfig) (*config, bool) {
|
|||||||
} else {
|
} else {
|
||||||
cfg.Web.Port = *raw.Web.Port
|
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 {
|
if raw.Web.StaticDir == nil {
|
||||||
changed = true
|
changed = true
|
||||||
} else {
|
} 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)
|
return fmt.Errorf("invalid database.driver %q: must be sqlite or mysql", cfg.Database.Driver)
|
||||||
}
|
}
|
||||||
if cfg.Web.Enabled {
|
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)
|
return fmt.Errorf("invalid web port %d: must be 1-65535", cfg.Web.Port)
|
||||||
}
|
}
|
||||||
if cfg.Web.StaticDir == "" {
|
if cfg.Web.StaticDir == "" {
|
||||||
|
|||||||
+18
-1
@@ -38,6 +38,9 @@ func TestLoadConfigCreatesDefaultFile(t *testing.T) {
|
|||||||
if cfg.Web.Port != 8080 {
|
if cfg.Web.Port != 8080 {
|
||||||
t.Fatalf("web port = %d, want 8080", cfg.Web.Port)
|
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" {
|
if cfg.Web.StaticDir != "./dist" {
|
||||||
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
|
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
|
||||||
}
|
}
|
||||||
@@ -77,7 +80,7 @@ func TestLoadConfigFillsMissingFields(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
text := string(data)
|
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) {
|
if !strings.Contains(text, want) {
|
||||||
t.Fatalf("completed config missing %q in:\n%s", want, text)
|
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) {
|
func TestValidateConfigWeb(t *testing.T) {
|
||||||
cfg := defaultConfig()
|
cfg := defaultConfig()
|
||||||
|
cfg.Web.SocketPath = ""
|
||||||
cfg.Web.Port = 0
|
cfg.Web.Port = 0
|
||||||
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
|
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
|
||||||
t.Fatalf("invalid web port error = %v, want web port error", err)
|
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 = defaultConfig()
|
||||||
cfg.Web.StaticDir = ""
|
cfg.Web.StaticDir = ""
|
||||||
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") {
|
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") {
|
||||||
|
|||||||
+120
@@ -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}"
|
||||||
@@ -214,6 +214,7 @@ func parseArgs() (*config, error) {
|
|||||||
flag.BoolVar(&cfg.Web.Enabled, "web", cfg.Web.Enabled, "Enable Gin web server")
|
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.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.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.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.StringVar(&cfg.Web.Admin.Username, "admin-username", cfg.Web.Admin.Username, "Web admin username")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -267,12 +268,22 @@ func run(cfg *config) error {
|
|||||||
}
|
}
|
||||||
mqttStatus := mqttRuntimeStatus{server: server, address: mqttAddr, tls: cfg.MQTT.TLS.Enabled, stats: messageStats}
|
mqttStatus := mqttRuntimeStatus{server: server, address: mqttAddr, tls: cfg.MQTT.TLS.Enabled, stats: messageStats}
|
||||||
httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking)
|
httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking)
|
||||||
|
webAddress := httpServer.Addr
|
||||||
go func() {
|
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) {
|
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
errCh <- err
|
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)
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
|||||||
@@ -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 {
|
func newRouter(cfg webConfig, store *store, sessions *sessionManager, mqttStatus mqttStatusProvider, blocking *blockingCache) *gin.Engine {
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Logger(), gin.Recovery())
|
r.Use(gin.Logger(), gin.Recovery())
|
||||||
|
|||||||
Reference in New Issue
Block a user