支持 Web 端口和 Sock 同时启用

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-16 19:36:47 +08:00
co-authored by Claude
parent f00f030ef3
commit 0f04047517
3 changed files with 152 additions and 40 deletions
+48 -7
View File
@@ -52,6 +52,8 @@ type mysqlConfig struct {
type webConfig struct {
Enabled bool `yaml:"enabled"`
PortEnabled bool `yaml:"port_enabled"`
SocketEnabled bool `yaml:"socket_enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
SocketPath string `yaml:"socket_path"`
@@ -106,6 +108,8 @@ type rawMySQLConfig struct {
type rawWebConfig struct {
Enabled *bool `yaml:"enabled"`
PortEnabled *bool `yaml:"port_enabled"`
SocketEnabled *bool `yaml:"socket_enabled"`
Host *string `yaml:"host"`
Port *int `yaml:"port"`
SocketPath *string `yaml:"socket_path"`
@@ -143,6 +147,8 @@ func defaultConfig() *config {
},
Web: webConfig{
Enabled: true,
PortEnabled: true,
SocketEnabled: defaultWebSocketPath() != "",
Host: "0.0.0.0",
Port: 8080,
SocketPath: defaultWebSocketPath(),
@@ -160,12 +166,20 @@ func defaultConfig() *config {
// defaultConfigDir 根据操作系统返回配置目录。
func defaultConfigDir() string {
if runtime.GOOS == "windows" {
return defaultConfigDirForGOOS(runtime.GOOS)
}
func defaultConfigDirForGOOS(goos string) string {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "etc", "mesh_mqtt_go")
}
return filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go")
}
func useRelativeDefaultPath(goos string) bool {
return goos == "windows" || goos == "darwin"
}
// defaultConfigPath 返回默认配置文件路径。
func defaultConfigPath() string {
return filepath.Join(defaultConfigDir(), configFileName)
@@ -184,7 +198,7 @@ func defaultMapTileCacheDir() string {
}
func defaultMapTileCacheDirForGOOS(goos string) string {
if goos == "windows" {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "srv", "mesh_mqtt_go")
}
return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
@@ -194,19 +208,30 @@ func defaultWebSocketPathForGOOS(goos string) string {
if goos == "windows" {
return ""
}
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
}
return filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
}
func clearWebSocketPathOnUnsupportedGOOS(cfg *config, goos string) bool {
if goos != "windows" || cfg.Web.SocketPath == "" {
if goos != "windows" {
return false
}
cfg.Web.SocketPath = ""
return true
changed := false
if cfg.Web.SocketPath != "" {
cfg.Web.SocketPath = ""
changed = true
}
if cfg.Web.SocketEnabled {
cfg.Web.SocketEnabled = false
changed = true
}
return changed
}
func defaultSQLitePathForGOOS(goos string) string {
if goos == "windows" {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
}
return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go", "mesh_mqtt_go.db")
@@ -336,6 +361,16 @@ func normalizeConfig(raw rawConfig) (*config, bool) {
} else {
cfg.Web.Enabled = *raw.Web.Enabled
}
if raw.Web.PortEnabled == nil {
changed = true
} else {
cfg.Web.PortEnabled = *raw.Web.PortEnabled
}
if raw.Web.SocketEnabled == nil {
changed = true
} else {
cfg.Web.SocketEnabled = *raw.Web.SocketEnabled
}
if raw.Web.Host == nil {
changed = true
} else {
@@ -407,9 +442,15 @@ 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.SocketPath == "" && (cfg.Web.Port <= 0 || cfg.Web.Port > 65535) {
if !cfg.Web.PortEnabled && !cfg.Web.SocketEnabled {
return fmt.Errorf("web.port_enabled and web.socket_enabled cannot both be false when web is enabled")
}
if cfg.Web.PortEnabled && (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.SocketEnabled && cfg.Web.SocketPath == "" {
return fmt.Errorf("web.socket_path is required when web.socket_enabled is true")
}
if cfg.Web.StaticDir == "" {
return fmt.Errorf("web.static_dir is required when web is enabled")
}
+71 -15
View File
@@ -35,6 +35,13 @@ func TestLoadConfigCreatesDefaultFile(t *testing.T) {
if !cfg.Web.Enabled {
t.Fatalf("web enabled = false, want true")
}
if !cfg.Web.PortEnabled {
t.Fatalf("web port enabled = false, want true")
}
wantSocketEnabled := defaultWebSocketPath() != ""
if cfg.Web.SocketEnabled != wantSocketEnabled {
t.Fatalf("web socket enabled = %t, want %t", cfg.Web.SocketEnabled, wantSocketEnabled)
}
if cfg.Web.Port != 8080 {
t.Fatalf("web port = %d, want 8080", cfg.Web.Port)
}
@@ -83,7 +90,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:", "socket_path:", "static_dir:", "map_tile_cache_dir:"} {
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port_enabled:", "socket_enabled:", "port:", "socket_path:", "static_dir:", "map_tile_cache_dir:"} {
if !strings.Contains(text, want) {
t.Fatalf("completed config missing %q in:\n%s", want, text)
}
@@ -117,7 +124,7 @@ func TestLoadConfigPreservesExplicitWebFalse(t *testing.T) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
content := "web:\n enabled: false\n host: 127.0.0.1\n port: 8081\n static_dir: ./public\n"
content := "web:\n enabled: false\n port_enabled: false\n socket_enabled: false\n host: 127.0.0.1\n port: 8081\n static_dir: ./public\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
@@ -129,6 +136,9 @@ func TestLoadConfigPreservesExplicitWebFalse(t *testing.T) {
if cfg.Web.Enabled {
t.Fatalf("web enabled = true, want explicit false")
}
if cfg.Web.PortEnabled || cfg.Web.SocketEnabled {
t.Fatalf("web listener enabled = %t/%t, want explicit false/false", cfg.Web.PortEnabled, cfg.Web.SocketEnabled)
}
if cfg.Web.Host != "127.0.0.1" || cfg.Web.Port != 8081 || cfg.Web.StaticDir != "./public" {
t.Fatalf("web config = %#v", cfg.Web)
}
@@ -157,11 +167,29 @@ func TestLoadConfigMalformedYAMLDoesNotOverwrite(t *testing.T) {
}
}
func TestDefaultConfigDirForGOOS(t *testing.T) {
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go")
for _, goos := range []string{"windows", "darwin"} {
path := defaultConfigDirForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s config dir = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultConfigDirForGOOS("linux")
wantLinux := filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go")
if linuxPath != wantLinux {
t.Fatalf("linux config dir = %q, want %q", linuxPath, wantLinux)
}
}
func TestDefaultMapTileCacheDirForGOOS(t *testing.T) {
windowsPath := defaultMapTileCacheDirForGOOS("windows")
wantWindows := filepath.Join(".", "win", "srv", "mesh_mqtt_go")
if windowsPath != wantWindows {
t.Fatalf("windows map tile cache dir = %q, want %q", windowsPath, wantWindows)
wantRelative := filepath.Join(".", "win", "srv", "mesh_mqtt_go")
for _, goos := range []string{"windows", "darwin"} {
path := defaultMapTileCacheDirForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s map tile cache dir = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultMapTileCacheDirForGOOS("linux")
@@ -176,6 +204,12 @@ func TestDefaultWebSocketPathForGOOS(t *testing.T) {
t.Fatalf("windows web socket path = %q, want empty", windowsPath)
}
darwinPath := defaultWebSocketPathForGOOS("darwin")
wantDarwin := filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
if darwinPath != wantDarwin {
t.Fatalf("darwin web socket path = %q, want %q", darwinPath, wantDarwin)
}
linuxPath := defaultWebSocketPathForGOOS("linux")
want := filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
if linuxPath != want {
@@ -192,6 +226,9 @@ func TestClearWebSocketPathOnUnsupportedGOOS(t *testing.T) {
if cfg.Web.SocketPath != "" {
t.Fatalf("windows web socket path = %q, want empty", cfg.Web.SocketPath)
}
if cfg.Web.SocketEnabled {
t.Fatalf("windows web socket enabled = true, want false")
}
cfg.Web.SocketPath = "/opt/mesh_mqtt_go/web.sock"
if clearWebSocketPathOnUnsupportedGOOS(cfg, "linux") {
@@ -203,9 +240,12 @@ func TestClearWebSocketPathOnUnsupportedGOOS(t *testing.T) {
}
func TestDefaultSQLitePathForGOOS(t *testing.T) {
windowsPath := defaultSQLitePathForGOOS("windows")
if !strings.Contains(windowsPath, filepath.Join("win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")) {
t.Fatalf("windows sqlite path = %q", windowsPath)
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
for _, goos := range []string{"windows", "darwin"} {
path := defaultSQLitePathForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s sqlite path = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultSQLitePathForGOOS("linux")
@@ -238,24 +278,38 @@ 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.SocketPath = filepath.Join(string(filepath.Separator), "tmp", "mesh_mqtt_go.sock")
cfg.Web.PortEnabled = false
cfg.Web.Port = 0
if err := validateConfig(cfg); err != nil {
t.Fatalf("web socket with invalid port error = %v, want nil", err)
t.Fatalf("disabled web port with invalid port error = %v, want nil", err)
}
cfg = defaultConfig()
cfg.Web.SocketEnabled = false
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)
if err := validateConfig(cfg); err != nil {
t.Fatalf("disabled web socket with empty path error = %v, want nil", err)
}
cfg = defaultConfig()
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = true
cfg.Web.SocketPath = ""
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.socket_path") {
t.Fatalf("missing web socket path error = %v, want web.socket_path error", err)
}
cfg = defaultConfig()
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = false
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.port_enabled") {
t.Fatalf("disabled web listeners error = %v, want web.port_enabled error", err)
}
cfg = defaultConfig()
@@ -272,6 +326,8 @@ func TestValidateConfigWeb(t *testing.T) {
cfg = defaultConfig()
cfg.Web.Enabled = false
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = false
cfg.Web.Port = 0
cfg.Web.StaticDir = ""
if err := validateConfig(cfg); err != nil {
+33 -18
View File
@@ -179,9 +179,11 @@ func parseArgs() (*config, error) {
flag.StringVar(&cfg.Database.SQLite.Path, "sqlite-path", cfg.Database.SQLite.Path, "SQLite database file path")
flag.StringVar(&cfg.Database.MySQL.DSN, "mysql-dsn", cfg.Database.MySQL.DSN, "MySQL database DSN")
flag.BoolVar(&cfg.Web.Enabled, "web", cfg.Web.Enabled, "Enable Gin web server")
flag.BoolVar(&cfg.Web.PortEnabled, "web-port-enabled", cfg.Web.PortEnabled, "Enable web server on TCP host and port")
flag.BoolVar(&cfg.Web.SocketEnabled, "web-socket-enabled", cfg.Web.SocketEnabled, "Enable web server on Unix socket; unsupported on Windows")
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; unsupported on Windows")
flag.StringVar(&cfg.Web.SocketPath, "web-socket-path", cfg.Web.SocketPath, "Web server Unix socket path; unsupported on Windows")
flag.StringVar(&cfg.Web.StaticDir, "web-static-dir", cfg.Web.StaticDir, "Web frontend static files directory")
flag.StringVar(&cfg.Web.MapTileCacheDir, "web-map-tile-cache-dir", cfg.Web.MapTileCacheDir, "Map tile disk cache root directory")
flag.StringVar(&cfg.Web.Admin.Username, "admin-username", cfg.Web.Admin.Username, "Web admin username")
@@ -245,31 +247,44 @@ func run(cfg *config) error {
}
defer forwardManager.StopAll()
var httpServer *http.Server
errCh := make(chan error, 1)
var httpServers []*http.Server
errCh := make(chan error, 2)
if cfg.Web.Enabled {
sessions, err := newSessionManager(cfg.Web.Admin)
if err != nil {
return err
}
mqttStatus := mqttRuntimeStatus{server: server, address: mqttAddr, tls: cfg.MQTT.TLS.Enabled, stats: messageStats, dbQueue: dbQueue}
httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking, forwardManager, settings, botSender)
webAddress := httpServer.Addr
go func() {
if cfg.Web.SocketPath != "" {
handler := newRouter(cfg.Web, store, sessions, mqttStatus, blocking, forwardManager, settings, botSender)
webAddresses := []string{}
if cfg.Web.PortEnabled {
httpServer := &http.Server{
Addr: net.JoinHostPort(cfg.Web.Host, strconv.Itoa(cfg.Web.Port)),
Handler: handler,
}
httpServers = append(httpServers, httpServer)
webAddresses = append(webAddresses, httpServer.Addr)
go func() {
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
}
if cfg.Web.SocketEnabled {
httpServer := &http.Server{Handler: handler}
httpServers = append(httpServers, httpServer)
webAddresses = append(webAddresses, cfg.Web.SocketPath)
go func() {
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
}
}()
if cfg.Web.SocketPath != "" {
webAddress = cfg.Web.SocketPath
}()
}
printJSON(map[string]any{"event": "web_started", "address": webAddress, "static_dir": cfg.Web.StaticDir})
webStarted := map[string]any{"event": "web_started", "addresses": webAddresses, "static_dir": cfg.Web.StaticDir}
if len(webAddresses) > 0 {
webStarted["address"] = webAddresses[0]
}
printJSON(webStarted)
}
sigCh := make(chan os.Signal, 1)
@@ -281,12 +296,12 @@ func run(cfg *config) error {
case runErr = <-errCh:
}
if httpServer != nil {
for _, httpServer := range httpServers {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil && runErr == nil {
runErr = err
}
cancel()
}
if err := server.Close(); err != nil && runErr == nil {
runErr = err