• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / go-mehrhof / 21375098520

26 Jan 2026 09:44PM UTC coverage: 40.173% (+0.1%) from 40.04%
21375098520

push

github

k0d3r1s
Enhances signing process for releases

Streamlines the signing configuration for releases by utilizing password-less signing.

This simplifies the release process by removing the need to explicitly pass the password during signing.

21721 of 54069 relevant lines covered (40.17%)

20.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.68
/internal/server/server.go
1
// Package server provides an HTTP server for the Mehrhof web UI.
2
package server
3

4
import (
5
        "context"
6
        "errors"
7
        "fmt"
8
        "log/slog"
9
        "net"
10
        "net/http"
11
        "sync"
12
        "time"
13

14
        "github.com/valksor/go-mehrhof/internal/conductor"
15
        "github.com/valksor/go-mehrhof/internal/storage"
16
        "github.com/valksor/go-toolkit/eventbus"
17
)
18

19
// Mode represents the server operating mode.
20
type Mode int
21

22
const (
23
        // ModeProject runs the server for a single project.
24
        ModeProject Mode = iota
25
        // ModeGlobal runs the server showing all discovered projects.
26
        ModeGlobal
27
)
28

29
// Config holds server configuration.
30
type Config struct {
31
        // Port specifies the port to listen on (0 = random available port).
32
        Port int
33
        // Host specifies the host to bind to (default: "localhost").
34
        Host string
35
        // Mode specifies whether to run in project or global mode.
36
        Mode Mode
37
        // Conductor is the conductor instance for project mode (nil for global mode).
38
        Conductor *conductor.Conductor
39
        // EventBus is the event bus for real-time updates.
40
        EventBus *eventbus.Bus
41
        // WorkspaceRoot is the root directory of the workspace (for project mode).
42
        WorkspaceRoot string
43
        // AuthStore is the authentication store (nil means no auth required).
44
        AuthStore *storage.AuthStore
45
}
46

47
// Server is the Mehrhof web UI HTTP server.
48
type Server struct {
49
        config     Config
50
        httpServer *http.Server
51
        listener   net.Listener
52
        router     http.Handler
53
        sessions   *sessionStore
54
        templates  *Templates
55

56
        mu                  sync.RWMutex
57
        running             bool
58
        actualPort          int
59
        startedInGlobalMode bool // Tracks if server originally started in global mode
60
}
61

62
// New creates a new server with the given configuration.
63
func New(cfg Config) (*Server, error) {
155✔
64
        // Create EventBus if not provided (for global mode)
155✔
65
        // In project mode, the conductor provides the EventBus, but in global mode
155✔
66
        // we need our own for SSE connections to work properly.
155✔
67
        if cfg.EventBus == nil {
309✔
68
                cfg.EventBus = eventbus.NewBus()
154✔
69
        }
154✔
70

71
        s := &Server{
155✔
72
                config:              cfg,
155✔
73
                sessions:            newSessionStore(),
155✔
74
                startedInGlobalMode: cfg.Mode == ModeGlobal,
155✔
75
        }
155✔
76

155✔
77
        // Load templates
155✔
78
        templates, err := LoadTemplates()
155✔
79
        if err != nil {
155✔
80
                slog.Warn("failed to load templates, using fallback UI", "error", err)
×
81
        } else {
155✔
82
                s.templates = templates
155✔
83
        }
155✔
84

85
        // Create router
86
        s.router = s.setupRouter()
155✔
87

155✔
88
        return s, nil
155✔
89
}
90

91
// Start starts the server and blocks until the context is cancelled.
92
func (s *Server) Start(ctx context.Context) error {
148✔
93
        // Create listener
148✔
94
        host := s.config.Host
148✔
95
        if host == "" {
296✔
96
                host = "localhost"
148✔
97
        }
148✔
98
        addr := fmt.Sprintf("%s:%d", host, s.config.Port)
148✔
99
        lc := net.ListenConfig{}
148✔
100
        listener, err := lc.Listen(ctx, "tcp", addr)
148✔
101
        if err != nil {
148✔
102
                return fmt.Errorf("listen on %s: %w", addr, err)
×
103
        }
×
104
        s.listener = listener
148✔
105

148✔
106
        // Extract actual port (important for random port allocation)
148✔
107
        tcpAddr, ok := listener.Addr().(*net.TCPAddr)
148✔
108
        if !ok {
148✔
109
                return fmt.Errorf("unexpected listener address type: %T", listener.Addr())
×
110
        }
×
111

112
        // Create HTTP server
113
        s.httpServer = &http.Server{
148✔
114
                Handler:      s.router,
148✔
115
                ReadTimeout:  30 * time.Second,
148✔
116
                WriteTimeout: 30 * time.Second,
148✔
117
                IdleTimeout:  60 * time.Second,
148✔
118
        }
148✔
119

148✔
120
        s.mu.Lock()
148✔
121
        s.actualPort = tcpAddr.Port
148✔
122
        s.running = true
148✔
123
        s.mu.Unlock()
148✔
124

148✔
125
        // Log server start
148✔
126
        slog.Info("server started", "port", s.actualPort, "mode", s.modeString())
148✔
127

148✔
128
        // Handle graceful shutdown
148✔
129
        errCh := make(chan error, 1)
148✔
130
        go func() {
296✔
131
                errCh <- s.httpServer.Serve(listener)
148✔
132
        }()
148✔
133

134
        select {
148✔
135
        case err := <-errCh:
1✔
136
                if err != nil && !errors.Is(err, http.ErrServerClosed) {
1✔
137
                        return fmt.Errorf("serve: %w", err)
×
138
                }
×
139
        case <-ctx.Done():
147✔
140
                // Graceful shutdown - intentionally use Background() since parent context is cancelled
147✔
141
                shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
147✔
142
                defer cancel()
147✔
143

147✔
144
                if err := s.httpServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck // shutdownCtx is derived from Background, intentionally
147✔
145
                        slog.Warn("server shutdown error", "error", err)
×
146
                }
×
147
        }
148

149
        s.mu.Lock()
148✔
150
        s.running = false
148✔
151
        s.mu.Unlock()
148✔
152

148✔
153
        slog.Info("server stopped")
148✔
154

148✔
155
        return nil
148✔
156
}
157

158
// Shutdown gracefully shuts down the server.
159
func (s *Server) Shutdown(ctx context.Context) error {
2✔
160
        s.mu.Lock()
2✔
161

2✔
162
        if !s.running {
3✔
163
                s.mu.Unlock()
1✔
164

1✔
165
                return nil
1✔
166
        }
1✔
167

168
        if s.httpServer != nil {
2✔
169
                hs := s.httpServer
1✔
170
                s.mu.Unlock()
1✔
171

1✔
172
                return hs.Shutdown(ctx)
1✔
173
        }
1✔
174

175
        s.mu.Unlock()
×
176

×
177
        return nil
×
178
}
179

180
// Port returns the actual port the server is listening on.
181
// Returns 0 if the server is not running.
182
func (s *Server) Port() int {
369✔
183
        s.mu.RLock()
369✔
184
        defer s.mu.RUnlock()
369✔
185

369✔
186
        return s.actualPort
369✔
187
}
369✔
188

189
// URL returns the full URL to access the server.
190
func (s *Server) URL() string {
179✔
191
        host := s.config.Host
179✔
192
        if host == "" || host == "0.0.0.0" {
358✔
193
                host = "localhost"
179✔
194
        }
179✔
195

196
        return fmt.Sprintf("http://%s:%d", host, s.Port())
179✔
197
}
198

199
// IsRunning returns true if the server is running.
200
func (s *Server) IsRunning() bool {
9✔
201
        s.mu.RLock()
9✔
202
        defer s.mu.RUnlock()
9✔
203

9✔
204
        return s.running
9✔
205
}
9✔
206

207
// modeString returns a human-readable string for the server mode.
208
func (s *Server) modeString() string {
176✔
209
        switch s.config.Mode {
176✔
210
        case ModeProject:
151✔
211
                return "project"
151✔
212
        case ModeGlobal:
24✔
213
                return "global"
24✔
214
        default:
1✔
215
                return "unknown"
1✔
216
        }
217
}
218

219
// isLocalRequest returns true if the request originates from localhost.
220
// Used to determine whether to show sensitive data like API tokens.
221
func isLocalRequest(r *http.Request) bool {
17✔
222
        host, _, err := net.SplitHostPort(r.RemoteAddr)
17✔
223
        if err != nil {
17✔
224
                // If we can't parse, assume it's the host without port
×
225
                host = r.RemoteAddr
×
226
        }
×
227

228
        // Check for loopback addresses
229
        if host == "127.0.0.1" || host == "::1" || host == "localhost" {
34✔
230
                return true
17✔
231
        }
17✔
232

233
        // Also check if the IP is a loopback
234
        ip := net.ParseIP(host)
×
235
        if ip != nil && ip.IsLoopback() {
×
236
                return true
×
237
        }
×
238

239
        return false
×
240
}
241

242
// switchToProject switches the server from global mode to project mode.
243
// This updates the config, creates a conductor for the project, and rebuilds the router.
244
func (s *Server) switchToProject(projectPath string) error {
×
245
        s.mu.Lock()
×
246
        defer s.mu.Unlock()
×
247

×
248
        // Create a new conductor for the project
×
249
        cond, err := conductor.New(conductor.WithWorkDir(projectPath))
×
250
        if err != nil {
×
251
                return fmt.Errorf("create conductor: %w", err)
×
252
        }
×
253

254
        // Update server config
255
        s.config.Mode = ModeProject
×
256
        s.config.WorkspaceRoot = projectPath
×
257
        s.config.Conductor = cond
×
258

×
259
        // Rebuild router with new mode
×
260
        s.router = s.setupRouter()
×
261

×
262
        // Update the http server's handler
×
263
        if s.httpServer != nil {
×
264
                s.httpServer.Handler = s.router
×
265
        }
×
266

267
        return nil
×
268
}
269

270
// switchToGlobal switches the server back to global mode.
271
func (s *Server) switchToGlobal() {
×
272
        s.mu.Lock()
×
273
        defer s.mu.Unlock()
×
274

×
275
        // Update server config
×
276
        s.config.Mode = ModeGlobal
×
277
        s.config.WorkspaceRoot = ""
×
278
        s.config.Conductor = nil
×
279

×
280
        // Rebuild router with global mode
×
281
        s.router = s.setupRouter()
×
282

×
283
        // Update the http server's handler
×
284
        if s.httpServer != nil {
×
285
                s.httpServer.Handler = s.router
×
286
        }
×
287
}
288

289
// canSwitchProject returns true if the server can switch between projects.
290
func (s *Server) canSwitchProject() bool {
20✔
291
        s.mu.RLock()
20✔
292
        defer s.mu.RUnlock()
20✔
293

20✔
294
        return s.startedInGlobalMode
20✔
295
}
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc