vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
a5f19b3c
Keenan Nemetz
18d ago
|
1 | // Package server wires the components together and owns the listeners. |
|
653e0cfc
Keenan Nemetz
18d ago
|
2 | package server |
| 3 | ||
| 4 | import ( | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
5 | "context" |
| 6 | "errors" | |
|
653e0cfc
Keenan Nemetz
18d ago
|
7 | "fmt" |
| 8 | "log/slog" | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
9 | "net" |
|
653e0cfc
Keenan Nemetz
18d ago
|
10 | "net/http" |
|
a5f19b3c
Keenan Nemetz
18d ago
|
11 | "strconv" |
|
642e5994
Keenan Nemetz
14d ago
|
12 | "strings" |
|
a5f19b3c
Keenan Nemetz
18d ago
|
13 | "time" |
|
653e0cfc
Keenan Nemetz
18d ago
|
14 | |
|
d37d301f
Keenan Nemetz
19h ago
|
15 | "rodent.software/vole/ci/runs" |
| 16 | "rodent.software/vole/core/lfs" | |
| 17 | "rodent.software/vole/forge" | |
| 18 | "rodent.software/vole/forge/config" | |
| 19 | "rodent.software/vole/forge/identity" | |
| 20 | "rodent.software/vole/forge/index" | |
| 21 | "rodent.software/vole/forge/instancerepo" | |
| 22 | "rodent.software/vole/forge/pageview" | |
| 23 | "rodent.software/vole/forge/repo" | |
| 24 | "rodent.software/vole/forge/secret" | |
| 25 | "rodent.software/vole/forge/session" | |
| 26 | "rodent.software/vole/internal/compress" | |
| 27 | "rodent.software/vole/server/githttp" | |
| 28 | "rodent.software/vole/server/pages" | |
| 29 | "rodent.software/vole/server/ssh" | |
| 30 | "rodent.software/vole/server/web" | |
|
653e0cfc
Keenan Nemetz
18d ago
|
31 | ) |
| 32 | ||
| 33 | type Server struct { | |
|
4506a4d3
Keenan Nemetz
14d ago
|
34 | ctx context.Context |
| 35 | cancel context.CancelFunc | |
| 36 | Cfg *config.Config | |
| 37 | Repos *repo.Store | |
| 38 | Identity *identity.Store | |
| 39 | Sessions *session.Store | |
| 40 | Runs *runs.Store | |
| 41 | Index *index.Store | |
| 42 | LFS *lfs.Store | |
| 43 | Locks *lfs.LockStore | |
| 44 | PageViews *pageview.Store | |
|
6ce635a8
Keenan Nemetz
1d ago
|
45 | // InstanceRepo is the repository whose tree customizes the instance. |
| 46 | InstanceRepo *instancerepo.Store | |
|
0c40a1d3
Keenan Nemetz
7d ago
|
47 | // Secrets is the per-repository store behind the settings page and the |
| 48 | // `vole secret` verbs. Values never leave it except to a caller it has audited. | |
| 49 | Secrets *secret.Store | |
| 50 | Pages *pages.Server | |
| 51 | Proxy *githttp.Proxy | |
| 52 | SSH *ssh.Server | |
| 53 | Web *web.Web | |
| 54 | Log *slog.Logger | |
| 55 | mux *http.ServeMux | |
| 56 | acme *acmeSetup | |
|
eb07ff56
Keenan Nemetz
8d ago
|
57 | // metadataWake nudges the metadata event loop after a browser write. Pushes |
| 58 | // deliver the same information through the spool; this is the path a write made | |
| 59 | // without a push uses. | |
| 60 | metadataWake chan struct{} | |
|
653e0cfc
Keenan Nemetz
18d ago
|
61 | } |
| 62 | ||
| 63 | func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) { | |
| 64 | if log == nil { | |
| 65 | log = slog.Default() | |
| 66 | } | |
|
ed181212
Keenan Nemetz
13d ago
|
67 | // Refuse an unsafe configuration before anything is opened: a listener that |
| 68 | // must not be reachable, a development login next to a public URL, a size that | |
| 69 | // cannot be satisfied. A warning here would be read by nobody. | |
| 70 | if err := cfg.Validate(); err != nil { | |
| 71 | return nil, err | |
| 72 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
73 | inst, err := forge.Open(cfg, binPath) |
|
b44cbfaf
Keenan Nemetz
17d ago
|
74 | if err != nil { |
|
c5902451
Keenan Nemetz
16d ago
|
75 | return nil, err |
|
bf1e027c
Keenan Nemetz
16d ago
|
76 | } |
|
ed181212
Keenan Nemetz
13d ago
|
77 | // A secret that is group- or world-readable is a secret already disclosed. |
| 78 | // Tighten what an older install left, say so, and refuse to run when the mode | |
| 79 | // cannot be changed: an instance that cannot protect its keys must not serve. | |
| 80 | if tightened, err := forge.TightenSecrets(cfg); err != nil { | |
| 81 | inst.Close() | |
| 82 | return nil, err | |
| 83 | } else if len(tightened) > 0 { | |
| 84 | log.Warn("tightened permissions on secret files", "paths", strings.Join(tightened, ", ")) | |
| 85 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
86 | if inst.ImportedACLs > 0 { |
| 87 | log.Info("access rules imported", "repos", inst.ImportedACLs) | |
|
ec5c519c
Keenan Nemetz
17d ago
|
88 | } |
|
c5902451
Keenan Nemetz
16d ago
|
89 | if n, err := inst.Repos.InstallAllHooks(); err != nil { |
|
653e0cfc
Keenan Nemetz
18d ago
|
90 | log.Warn("install hooks", "err", err) |
|
a5f19b3c
Keenan Nemetz
18d ago
|
91 | } else if n > 0 { |
|
653e0cfc
Keenan Nemetz
18d ago
|
92 | log.Info("hooks installed", "repos", n) |
| 93 | } | |
|
642e5994
Keenan Nemetz
14d ago
|
94 | acme, err := newACME(cfg, inst.Repos.Public) |
| 95 | if err != nil { | |
| 96 | inst.Close() | |
| 97 | return nil, err | |
| 98 | } | |
| 99 | if acme != nil { | |
| 100 | log.Info("automatic HTTPS enabled", "addr", cfg.HTTPSAddr, "hosts", strings.Join(acme.hostNames(), ", "), "cache", cfg.ACMECachePath()) | |
| 101 | if cfg.PagesDomain != "" { | |
| 102 | log.Info("site certificates issued on demand", "domain", cfg.PagesDomain) | |
| 103 | } | |
| 104 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
105 | proxy := githttp.New(githttp.Config{ |
|
86991e70
Keenan Nemetz
13d ago
|
106 | Root: cfg.ReposDir(), |
| 107 | Open: inst.Repos.Open, | |
| 108 | Log: log, | |
| 109 | MaxConcurrent: cfg.GitHTTPMaxConcurrent, | |
| 110 | MaxPerClient: cfg.GitHTTPMaxPerClient, | |
| 111 | ClientIPHeader: cfg.GitHTTPClientIPHeader, | |
| 112 | RequestTimeout: time.Duration(cfg.GitHTTPTimeoutSeconds) * time.Second, | |
|
653e0cfc
Keenan Nemetz
18d ago
|
113 | }) |
|
c5902451
Keenan Nemetz
16d ago
|
114 | ssh, err := ssh.New(ssh.Config{ |
|
9e8cc155
Keenan Nemetz
12d ago
|
115 | Identity: inst.Identity, |
| 116 | Repos: inst.Repos, | |
| 117 | Instance: inst, | |
|
5ece70a5
Keenan Nemetz
10d ago
|
118 | Logs: inst.Logs, |
|
9e8cc155
Keenan Nemetz
12d ago
|
119 | LFS: inst.LFS, |
| 120 | LFSWarnBytes: cfg.LFSWarnBytes, | |
| 121 | Log: log, | |
| 122 | HostKeyPath: cfg.SSHHostKey(), | |
| 123 | MaxBlobBytes: cfg.MaxBlobBytes, | |
| 124 | MaxRefs: cfg.RepoMaxRefs, | |
| 125 | MaxMetadataEntries: cfg.RepoMaxMetadataEntries, | |
| 126 | MaxMetadataBytes: cfg.RepoMaxMetadataBytes, | |
| 127 | MaxRepoBytes: cfg.RepoMaxBytes, | |
| 128 | MaxLFSBytes: cfg.RepoMaxLFSBytes, | |
| 129 | AuditPath: cfg.AuditLog(), | |
| 130 | SpoolDir: cfg.SpoolDir(), | |
| 131 | RunsDB: cfg.RunsDB(), | |
|
dced06da
Keenan Nemetz
10d ago
|
132 | IndexDB: cfg.IndexDB(), |
|
9e8cc155
Keenan Nemetz
12d ago
|
133 | Runs: inst.Runs, |
| 134 | Locks: inst.Locks, | |
| 135 | Lease: cfg.Lease(), | |
| 136 | MaxJobsPerRepo: cfg.MaxJobsPerRepo, | |
| 137 | PagesRef: cfg.PagesRef, | |
| 138 | PublicURL: cfg.BaseURL(), | |
| 139 | MaxConnections: cfg.SSHMaxConnections, | |
| 140 | MaxGitProcesses: cfg.SSHMaxGitProcesses, | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
141 | }) |
| 142 | if err != nil { | |
|
c5902451
Keenan Nemetz
16d ago
|
143 | inst.Close() |
|
a5f19b3c
Keenan Nemetz
18d ago
|
144 | return nil, fmt.Errorf("ssh: %w", err) |
| 145 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
146 | wp, err := web.New(inst.Repos, inst.Index, inst.Identity, inst.Runs, cfg) |
|
653e0cfc
Keenan Nemetz
18d ago
|
147 | if err != nil { |
|
c5902451
Keenan Nemetz
16d ago
|
148 | inst.Close() |
|
653e0cfc
Keenan Nemetz
18d ago
|
149 | return nil, err |
| 150 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
151 | wp.LFS = inst.LFS |
| 152 | wp.Locks = inst.Locks | |
|
6c84d387
Keenan Nemetz
16d ago
|
153 | wp.Sessions = inst.Sessions |
|
4506a4d3
Keenan Nemetz
14d ago
|
154 | wp.PageViews = inst.PageViews |
|
6ce635a8
Keenan Nemetz
1d ago
|
155 | wp.InstanceRepo = inst.InstanceRepo |
|
0c40a1d3
Keenan Nemetz
7d ago
|
156 | wp.Secrets = inst.Secrets |
|
653e0cfc
Keenan Nemetz
18d ago
|
157 | wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) } |
|
f09ef39e
Keenan Nemetz
15d ago
|
158 | if cfg.DevLogin != "" { |
| 159 | log.Warn("development login enabled", "account", cfg.DevLogin, "http_addr", cfg.HTTPAddr) | |
| 160 | } | |
|
6c84d387
Keenan Nemetz
16d ago
|
161 | if wp.Auth != nil && !wp.Auth.Enabled() { |
| 162 | log.Info("browser login disabled", "reason", wp.Auth.Disabled) | |
|
f920a9f1
Keenan Nemetz
16d ago
|
163 | } else if cfg.OpenRegistration { |
|
ed03b3d1
Keenan Nemetz
6h ago
|
164 | log.Info("open registration enabled", "url", cfg.BaseURL()+"/-/register") |
|
6c84d387
Keenan Nemetz
16d ago
|
165 | } |
|
653e0cfc
Keenan Nemetz
18d ago
|
166 | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
167 | s := &Server{ |
|
6c84d387
Keenan Nemetz
16d ago
|
168 | Cfg: cfg, Repos: inst.Repos, Identity: inst.Identity, Sessions: inst.Sessions, |
|
642e5994
Keenan Nemetz
14d ago
|
169 | Runs: inst.Runs, Index: inst.Index, acme: acme, |
|
0c40a1d3
Keenan Nemetz
7d ago
|
170 | LFS: inst.LFS, Locks: inst.Locks, PageViews: inst.PageViews, Secrets: inst.Secrets, |
|
4506a4d3
Keenan Nemetz
14d ago
|
171 | Proxy: proxy, SSH: ssh, Web: wp, Log: log, |
|
eb07ff56
Keenan Nemetz
8d ago
|
172 | metadataWake: make(chan struct{}, 1), |
|
6ce635a8
Keenan Nemetz
1d ago
|
173 | InstanceRepo: inst.InstanceRepo, |
|
a5f19b3c
Keenan Nemetz
18d ago
|
174 | } |
|
5ece70a5
Keenan Nemetz
10d ago
|
175 | wp.Logs = inst.Logs |
|
6c84d387
Keenan Nemetz
16d ago
|
176 | wp.AllowCI = s.allowContributorCI |
|
e5478085
Keenan Nemetz
11d ago
|
177 | wp.EnsureCandidate = s.ensureCandidateForWeb |
|
eb07ff56
Keenan Nemetz
8d ago
|
178 | wp.MetadataChanged = s.wakeMetadata |
| 179 | wp.DispatchWorkflow = s.dispatchWorkflow | |
| 180 | ssh.SetDispatchWorkflow(s.dispatchWorkflow) | |
|
2973f727
Keenan Nemetz
15d ago
|
181 | wp.RenameRepo = inst.RenameRepo |
| 182 | wp.DeleteRepo = inst.DeleteRepo | |
|
d3296cda
Keenan Nemetz
17d ago
|
183 | // Sites are opt-in: without a pages domain there is no host to serve them on, |
| 184 | // and an instance that never wanted them pays no processes for the idea. | |
| 185 | if cfg.PagesDomain != "" { | |
| 186 | s.Pages = pages.New(pages.Config{ | |
| 187 | Domain: cfg.PagesDomain, | |
| 188 | Ref: cfg.PagesRef, | |
| 189 | Procs: cfg.PagesProcs, | |
| 190 | CacheBytes: cfg.PagesCacheBytes, | |
| 191 | Log: log, | |
|
c5902451
Keenan Nemetz
16d ago
|
192 | }, inst.Repos) |
|
d3296cda
Keenan Nemetz
17d ago
|
193 | } |
|
653e0cfc
Keenan Nemetz
18d ago
|
194 | s.mux = s.routes() |
|
b44cbfaf
Keenan Nemetz
17d ago
|
195 | // Background loops start here rather than in Run: a caller that serves the |
| 196 | // listeners itself (the tests, an embedder) must still get roster reloads, | |
| 197 | // queue ingestion and lease expiry. | |
| 198 | s.ctx, s.cancel = context.WithCancel(context.Background()) | |
|
6ce635a8
Keenan Nemetz
1d ago
|
199 | if s.InstanceRepo != nil && s.InstanceRepo.Enabled() { |
| 200 | if err := s.InstanceRepo.Reload(); err != nil { | |
| 201 | log.Warn("instance repository: cannot load", "repo", cfg.InstanceRepo, "err", err) | |
| 202 | } | |
| 203 | go s.watchInstanceRepo(s.ctx) | |
| 204 | } | |
|
b44cbfaf
Keenan Nemetz
17d ago
|
205 | go s.watchRoster(s.ctx) |
| 206 | go s.watchSpool(s.ctx) | |
|
a945ae55
Keenan Nemetz
17d ago
|
207 | go s.watchRefs(s.ctx) |
|
eb07ff56
Keenan Nemetz
8d ago
|
208 | go s.watchMetadata(s.ctx) |
| 209 | go s.watchSchedules(s.ctx) | |
|
e5478085
Keenan Nemetz
11d ago
|
210 | go s.watchCandidates(s.ctx) |
| 211 | go s.watchAutoMerges(s.ctx) | |
|
b44cbfaf
Keenan Nemetz
17d ago
|
212 | go s.watchLeases(s.ctx) |
|
65b88b3b
Keenan Nemetz
16d ago
|
213 | // An instance that stored objects before they were scoped by repository needs its |
| 214 | // links, and nobody reads an upgrade note. This is idempotent and deletes nothing. | |
| 215 | s.scopeLFSStore() | |
|
653e0cfc
Keenan Nemetz
18d ago
|
216 | return s, nil |
| 217 | } | |
| 218 | ||
|
d3296cda
Keenan Nemetz
17d ago
|
219 | // Handler returns the HTTP surface. Site hosts are dispatched first: a request |
| 220 | // for <repo>.<pages_domain> is a page, and everything else is the forge UI. | |
|
a76b2f38
Keenan Nemetz
12d ago
|
221 | // |
| 222 | // Sites are compressed here rather than inside the pages package because the | |
| 223 | // dispatcher is the last point both surfaces pass through, and a site is a static | |
| 224 | // file server: HTML, CSS and JavaScript, which is exactly what gzip is for. The | |
| 225 | // forge UI does its own, inside its middleware, because the compressor has to sit | |
| 226 | // under the status recorder there. | |
|
d3296cda
Keenan Nemetz
17d ago
|
227 | func (s *Server) Handler() http.Handler { |
|
ed03b3d1
Keenan Nemetz
6h ago
|
228 | forge := s.Web.Middleware(s.forgeHandler(s.mux)) |
|
d3296cda
Keenan Nemetz
17d ago
|
229 | if s.Pages == nil { |
|
6c84d387
Keenan Nemetz
16d ago
|
230 | return forge |
|
d3296cda
Keenan Nemetz
17d ago
|
231 | } |
|
a76b2f38
Keenan Nemetz
12d ago
|
232 | sites := compress.Handler(s.Pages) |
|
d3296cda
Keenan Nemetz
17d ago
|
233 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 234 | if _, ok := s.Pages.Host(r.Host); ok { | |
|
a76b2f38
Keenan Nemetz
12d ago
|
235 | sites.ServeHTTP(w, r) |
|
d3296cda
Keenan Nemetz
17d ago
|
236 | return |
| 237 | } | |
|
6c84d387
Keenan Nemetz
16d ago
|
238 | forge.ServeHTTP(w, r) |
|
d3296cda
Keenan Nemetz
17d ago
|
239 | }) |
| 240 | } | |
|
653e0cfc
Keenan Nemetz
18d ago
|
241 | |
|
73755a56
Keenan Nemetz
9d ago
|
242 | // drainGrace is how long a clean shutdown lets an already-leased job finish before |
| 243 | // closing the listeners. It is shorter than systemd's default stop timeout, which | |
| 244 | // leaves room for the HTTP server's own shutdown. Work that does not fit in the | |
| 245 | // window is still lost on a restart; a result that arrives inside it is not. | |
| 246 | const drainGrace = time.Minute | |
| 247 | ||
| 248 | // drain stops new leases and waits for the jobs already running to report. It is | |
| 249 | // best effort: a runner that died, or a build longer than the grace, is left for | |
| 250 | // the next process, which can requeue it from the durable lease. | |
| 251 | func (s *Server) drain() { | |
| 252 | if s.Runs == nil { | |
| 253 | return | |
| 254 | } | |
| 255 | s.Runs.Drain() | |
| 256 | n, err := s.Runs.Running() | |
| 257 | if err != nil { | |
| 258 | s.Log.Warn("drain: cannot count running jobs", "err", err) | |
| 259 | return | |
| 260 | } | |
| 261 | if n == 0 { | |
| 262 | return | |
| 263 | } | |
| 264 | s.Log.Info("draining running jobs", "jobs", n, "grace", drainGrace) | |
| 265 | deadline := time.Now().Add(drainGrace) | |
| 266 | for time.Now().Before(deadline) { | |
| 267 | time.Sleep(250 * time.Millisecond) | |
| 268 | n, err = s.Runs.Running() | |
| 269 | if err != nil { | |
| 270 | s.Log.Warn("drain: cannot count running jobs", "err", err) | |
| 271 | return | |
| 272 | } | |
| 273 | if n == 0 { | |
| 274 | s.Log.Info("running jobs finished; shutting down") | |
| 275 | return | |
| 276 | } | |
| 277 | } | |
| 278 | s.Log.Warn("drain grace expired with jobs still running", "jobs", n) | |
| 279 | } | |
| 280 | ||
|
642e5994
Keenan Nemetz
14d ago
|
281 | // Run starts the listeners and blocks until ctx is cancelled. With automatic |
| 282 | // HTTPS, the HTTP address only answers ACME challenges and redirects, and the | |
| 283 | // forge is served over TLS on HTTPSAddr. | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
284 | func (s *Server) Run(ctx context.Context) error { |
| 285 | sshLn, err := net.Listen("tcp", s.Cfg.SSHAddr) | |
| 286 | if err != nil { | |
| 287 | return fmt.Errorf("ssh listen %s: %w", s.Cfg.SSHAddr, err) | |
| 288 | } | |
| 289 | httpLn, err := net.Listen("tcp", s.Cfg.HTTPAddr) | |
| 290 | if err != nil { | |
| 291 | sshLn.Close() | |
| 292 | return fmt.Errorf("http listen %s: %w", s.Cfg.HTTPAddr, err) | |
| 293 | } | |
| 294 | ||
|
642e5994
Keenan Nemetz
14d ago
|
295 | httpSrv := &http.Server{ |
|
a5f19b3c
Keenan Nemetz
18d ago
|
296 | Handler: s.Handler(), |
| 297 | ReadHeaderTimeout: 15 * time.Second, | |
|
86991e70
Keenan Nemetz
13d ago
|
298 | // ReadTimeout bounds the request, body included. The only bodies on the |
| 299 | // HTTP surface are a login form, a comment and a fetch negotiation, all of | |
| 300 | // which are small and arrive long before this. | |
| 301 | ReadTimeout: 60 * time.Second, | |
| 302 | // WriteTimeout is deliberately unset: a clone, a release tarball, an LFS | |
| 303 | // object and a site file are streamed, and a wall-clock limit on writing the | |
| 304 | // response would truncate a legitimate large download. Those streams are | |
| 305 | // bounded where they are produced instead — the git transport by its own | |
| 306 | // request deadline, archives by a concurrency bound, downloads by a size cap. | |
| 307 | MaxHeaderBytes: 64 << 10, | |
| 308 | IdleTimeout: 120 * time.Second, | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
309 | } |
|
642e5994
Keenan Nemetz
14d ago
|
310 | var httpsLn net.Listener |
| 311 | var httpsSrv *http.Server | |
| 312 | if s.acme != nil { | |
| 313 | httpSrv.Handler = s.acme.httpHandler() | |
| 314 | httpsLn, err = net.Listen("tcp", s.Cfg.HTTPSAddr) | |
| 315 | if err != nil { | |
| 316 | httpLn.Close() | |
| 317 | sshLn.Close() | |
| 318 | return fmt.Errorf("https listen %s: %w", s.Cfg.HTTPSAddr, err) | |
| 319 | } | |
| 320 | httpsSrv = &http.Server{ | |
| 321 | Handler: s.Handler(), | |
| 322 | TLSConfig: s.acme.tlsConfig(), | |
| 323 | ReadHeaderTimeout: 15 * time.Second, | |
|
86991e70
Keenan Nemetz
13d ago
|
324 | ReadTimeout: 60 * time.Second, |
| 325 | MaxHeaderBytes: 64 << 10, | |
|
642e5994
Keenan Nemetz
14d ago
|
326 | IdleTimeout: 120 * time.Second, |
| 327 | } | |
| 328 | } | |
| 329 | ||
| 330 | errCh := make(chan error, 3) | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
331 | go func() { |
| 332 | if err := s.SSH.Serve(sshLn); err != nil { | |
| 333 | errCh <- fmt.Errorf("ssh serve: %w", err) | |
|
653e0cfc
Keenan Nemetz
18d ago
|
334 | } |
|
a5f19b3c
Keenan Nemetz
18d ago
|
335 | }() |
| 336 | go func() { | |
|
642e5994
Keenan Nemetz
14d ago
|
337 | if err := httpSrv.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) { |
|
a5f19b3c
Keenan Nemetz
18d ago
|
338 | errCh <- fmt.Errorf("http serve: %w", err) |
| 339 | } | |
| 340 | }() | |
|
642e5994
Keenan Nemetz
14d ago
|
341 | if httpsSrv != nil { |
| 342 | go func() { | |
| 343 | if err := httpsSrv.ServeTLS(httpsLn, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { | |
| 344 | errCh <- fmt.Errorf("https serve: %w", err) | |
| 345 | } | |
| 346 | }() | |
| 347 | } | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
348 | |
|
642e5994
Keenan Nemetz
14d ago
|
349 | if httpsSrv != nil { |
| 350 | s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(), | |
| 351 | "https", httpsLn.Addr().String(), "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count) | |
| 352 | } else { | |
| 353 | s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(), | |
| 354 | "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count) | |
| 355 | } | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
356 | |
| 357 | select { | |
| 358 | case <-ctx.Done(): | |
|
73755a56
Keenan Nemetz
9d ago
|
359 | // Let the runners finish what they already hold before the sockets go away. |
| 360 | // A deploy that cuts a job in its last step costs a whole second attempt. | |
| 361 | s.drain() | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
362 | sh, cancel := context.WithTimeout(context.Background(), 20*time.Second) |
| 363 | defer cancel() | |
| 364 | s.SSH.Close() | |
|
642e5994
Keenan Nemetz
14d ago
|
365 | httpSrv.Shutdown(sh) |
| 366 | if httpsSrv != nil { | |
| 367 | httpsSrv.Shutdown(sh) | |
| 368 | } | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
369 | return nil |
| 370 | case err := <-errCh: | |
| 371 | s.SSH.Close() | |
|
642e5994
Keenan Nemetz
14d ago
|
372 | httpSrv.Close() |
| 373 | if httpsSrv != nil { | |
| 374 | httpsSrv.Close() | |
| 375 | } | |
|
a5f19b3c
Keenan Nemetz
18d ago
|
376 | return err |
|
653e0cfc
Keenan Nemetz
18d ago
|
377 | } |
|
a5f19b3c
Keenan Nemetz
18d ago
|
378 | } |
| 379 | ||
|
197ecaa2
Keenan Nemetz
18d ago
|
380 | func (s *Server) watchRoster(ctx context.Context) { |
| 381 | t := time.NewTicker(5 * time.Second) | |
| 382 | defer t.Stop() | |
| 383 | for { | |
| 384 | select { | |
| 385 | case <-ctx.Done(): | |
| 386 | return | |
| 387 | case <-t.C: | |
|
31df8bae
Keenan Nemetz
17d ago
|
388 | before := s.Identity.Snapshot().Count |
|
197ecaa2
Keenan Nemetz
18d ago
|
389 | if err := s.Identity.Reload(); err != nil { |
| 390 | s.Log.Error("roster reload failed; keeping the previous snapshot", "err", err) | |
| 391 | continue | |
| 392 | } | |
|
31df8bae
Keenan Nemetz
17d ago
|
393 | if after := s.Identity.Snapshot().Count; after != before { |
|
427c4761
Keenan Nemetz
17d ago
|
394 | s.Log.Info("roster reloaded", "accounts", after) |
|
197ecaa2
Keenan Nemetz
18d ago
|
395 | } |
| 396 | } | |
| 397 | } | |
| 398 | } | |
| 399 | ||
|
a5f19b3c
Keenan Nemetz
18d ago
|
400 | func (s *Server) Close() error { |
|
b44cbfaf
Keenan Nemetz
17d ago
|
401 | if s.cancel != nil { |
| 402 | s.cancel() | |
| 403 | } | |
|
d3296cda
Keenan Nemetz
17d ago
|
404 | if s.Pages != nil { |
| 405 | s.Pages.Close() | |
| 406 | } | |
|
b44cbfaf
Keenan Nemetz
17d ago
|
407 | if s.Runs != nil { |
| 408 | _ = s.Runs.Close() | |
| 409 | } | |
|
bf1e027c
Keenan Nemetz
16d ago
|
410 | if s.Locks != nil { |
| 411 | _ = s.Locks.Close() | |
| 412 | } | |
|
4506a4d3
Keenan Nemetz
14d ago
|
413 | if s.PageViews != nil { |
| 414 | _ = s.PageViews.Close() | |
| 415 | } | |
|
653e0cfc
Keenan Nemetz
18d ago
|
416 | if s.Index != nil { |
|
427c4761
Keenan Nemetz
17d ago
|
417 | if err := s.Index.Close(); err != nil { |
| 418 | return err | |
| 419 | } | |
| 420 | } | |
|
6c84d387
Keenan Nemetz
16d ago
|
421 | if s.Sessions != nil { |
| 422 | _ = s.Sessions.Close() | |
| 423 | } | |
|
427c4761
Keenan Nemetz
17d ago
|
424 | if s.Identity != nil { |
| 425 | return s.Identity.Close() | |
|
653e0cfc
Keenan Nemetz
18d ago
|
426 | } |
|
a5f19b3c
Keenan Nemetz
18d ago
|
427 | return nil |
|
653e0cfc
Keenan Nemetz
18d ago
|
428 | } |
| 429 | ||
| 430 | func (s *Server) RebuildIndex() (int, error) { return s.Index.Rebuild(s.Repos) } | |
| 431 | ||
|
a5f19b3c
Keenan Nemetz
18d ago
|
432 | // MaxInputLabel is used by logging to show the effective push cap. |
| 433 | func (s *Server) MaxInputLabel() string { | |
| 434 | return strconv.FormatInt(s.Cfg.ReceiveMaxBytes, 10) | |
|
653e0cfc
Keenan Nemetz
18d ago
|
435 | } |