fork of whitequark.org/git-pages with mods for tangled

Consistently use context in `Main()`.

Without this, some of the slog lines end in `\n` and some do not, which
I find deeply irritating.

Changed files
+84 -70
src
+1 -1
src/backend_fs.go
··· 212 return filepath.Join(domain, ".frozen") 213 } 214 215 - func (fs *FSBackend) checkDomainFrozen(_ctx context.Context, domain string) error { 216 if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil { 217 return ErrDomainFrozen 218 } else if !errors.Is(err, os.ErrNotExist) {
··· 212 return filepath.Join(domain, ".frozen") 213 } 214 215 + func (fs *FSBackend) checkDomainFrozen(ctx context.Context, domain string) error { 216 if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil { 217 return ErrDomainFrozen 218 } else if !errors.Is(err, os.ErrNotExist) {
+13 -2
src/log.go
··· 4 "context" 5 "fmt" 6 "log/slog" 7 "runtime" 8 "time" 9 ) ··· 25 // skip [runtime.Callers, this method, method calling this method] 26 runtime.Callers(3, pcs[:]) 27 28 - r := slog.NewRecord(time.Now(), level, msg, pcs[0]) 29 - logger.Handler().Handle(ctx, r) 30 } 31 32 func (l slogWithCtx) Print(ctx context.Context, v ...any) { ··· 40 func (l slogWithCtx) Println(ctx context.Context, v ...any) { 41 l.log(ctx, slog.LevelInfo, fmt.Sprintln(v...)) 42 }
··· 4 "context" 5 "fmt" 6 "log/slog" 7 + "os" 8 "runtime" 9 "time" 10 ) ··· 26 // skip [runtime.Callers, this method, method calling this method] 27 runtime.Callers(3, pcs[:]) 28 29 + record := slog.NewRecord(time.Now(), level, msg, pcs[0]) 30 + logger.Handler().Handle(ctx, record) 31 } 32 33 func (l slogWithCtx) Print(ctx context.Context, v ...any) { ··· 41 func (l slogWithCtx) Println(ctx context.Context, v ...any) { 42 l.log(ctx, slog.LevelInfo, fmt.Sprintln(v...)) 43 } 44 + 45 + func (l slogWithCtx) Fatalf(ctx context.Context, format string, v ...any) { 46 + l.log(ctx, slog.LevelError, fmt.Sprintf(format, v...)) 47 + os.Exit(1) 48 + } 49 + 50 + func (l slogWithCtx) Fatalln(ctx context.Context, v ...any) { 51 + l.log(ctx, slog.LevelError, fmt.Sprintln(v...)) 52 + os.Exit(1) 53 + }
+70 -67
src/main.go
··· 27 var fallback http.Handler 28 var backend Backend 29 30 - func configureFeatures() (err error) { 31 if len(config.Features) > 0 { 32 - log.Println("features:", strings.Join(config.Features, ", ")) 33 } 34 return 35 } 36 37 - func configureMemLimit() (err error) { 38 // Avoid being OOM killed by not garbage collecting early enough. 39 memlimitBefore := datasize.ByteSize(debug.SetMemoryLimit(-1)) 40 automemlimit.SetGoMemLimitWithOpts( ··· 49 ) 50 memlimitAfter := datasize.ByteSize(debug.SetMemoryLimit(-1)) 51 if memlimitBefore == memlimitAfter { 52 - log.Println("memlimit: now", memlimitBefore.HR()) 53 } else { 54 - log.Println("memlimit: was", memlimitBefore.HR(), "now", memlimitAfter.HR()) 55 } 56 return 57 } 58 59 - func configureWildcards() (err error) { 60 newWildcards, err := TranslateWildcards(config.Wildcard) 61 if err != nil { 62 return err ··· 66 } 67 } 68 69 - func configureFallback() (err error) { 70 if config.Fallback.ProxyTo != "" { 71 var fallbackURL *url.URL 72 fallbackURL, err = url.Parse(config.Fallback.ProxyTo) ··· 91 return 92 } 93 94 - func listen(name string, listen string) net.Listener { 95 if listen == "-" { 96 return nil 97 } 98 99 protocol, address, ok := strings.Cut(listen, "/") 100 if !ok { 101 - log.Fatalf("%s: %s: malformed endpoint", name, listen) 102 } 103 104 listener, err := net.Listen(protocol, address) 105 if err != nil { 106 - log.Fatalf("%s: %s\n", name, err) 107 } 108 109 return listener ··· 125 }) 126 } 127 128 - func serve(listener net.Listener, handler http.Handler) { 129 if listener != nil { 130 handler = panicHandler(handler) 131 ··· 135 if config.Feature("serve-h2c") { 136 server.Protocols.SetUnencryptedHTTP2(true) 137 } 138 - log.Fatalln(server.Serve(listener)) 139 } 140 } 141 ··· 146 case 1: 147 return arg 148 default: 149 - log.Fatalf("webroot argument must be either 'domain.tld' or 'domain.tld/dir") 150 return "" 151 } 152 } ··· 158 } else { 159 writer, err = os.Create(flag.Arg(0)) 160 if err != nil { 161 - log.Fatalln(err) 162 } 163 } 164 return ··· 178 } 179 180 func Main() { 181 flag.Usage = usage 182 printConfigEnvVars := flag.Bool("print-config-env-vars", false, 183 "print every recognized configuration environment variable and exit") ··· 226 cliOperations += 1 227 } 228 if cliOperations > 1 { 229 - log.Fatalln("-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive") 230 } 231 232 if *configTomlPath != "" && *noConfig { 233 - log.Fatalln("-no-config and -config are mutually exclusive") 234 } 235 236 if *printConfigEnvVars { ··· 243 *configTomlPath = "config.toml" 244 } 245 if config, err = Configure(*configTomlPath); err != nil { 246 - log.Fatalln("config:", err) 247 } 248 249 if *printConfig { ··· 255 defer FiniObservability() 256 257 if err = errors.Join( 258 - configureFeatures(), 259 - configureMemLimit(), 260 - configureWildcards(), 261 - configureFallback(), 262 ); err != nil { 263 - log.Fatalln(err) 264 } 265 266 switch { 267 case *runMigration != "": 268 if backend, err = CreateBackend(&config.Storage); err != nil { 269 - log.Fatalln(err) 270 } 271 272 - if err := RunMigration(context.Background(), *runMigration); err != nil { 273 - log.Fatalln(err) 274 } 275 276 case *getBlob != "": 277 if backend, err = CreateBackend(&config.Storage); err != nil { 278 - log.Fatalln(err) 279 } 280 281 - reader, _, _, err := backend.GetBlob(context.Background(), *getBlob) 282 if err != nil { 283 - log.Fatalln(err) 284 } 285 io.Copy(fileOutputArg(), reader) 286 287 case *getManifest != "": 288 if backend, err = CreateBackend(&config.Storage); err != nil { 289 - log.Fatalln(err) 290 } 291 292 webRoot := webRootArg(*getManifest) 293 - manifest, _, err := backend.GetManifest(context.Background(), webRoot, GetManifestOptions{}) 294 if err != nil { 295 - log.Fatalln(err) 296 } 297 fmt.Fprintln(fileOutputArg(), ManifestDebugJSON(manifest)) 298 299 case *getArchive != "": 300 if backend, err = CreateBackend(&config.Storage); err != nil { 301 - log.Fatalln(err) 302 } 303 304 webRoot := webRootArg(*getArchive) 305 manifest, manifestMtime, err := 306 - backend.GetManifest(context.Background(), webRoot, GetManifestOptions{}) 307 if err != nil { 308 - log.Fatalln(err) 309 } 310 - CollectTar(context.Background(), fileOutputArg(), manifest, manifestMtime) 311 312 case *updateSite != "": 313 if backend, err = CreateBackend(&config.Storage); err != nil { 314 - log.Fatalln(err) 315 } 316 317 if flag.NArg() != 1 { 318 - log.Fatalln("update source must be provided as the argument") 319 } 320 321 sourceURL, err := url.Parse(flag.Arg(0)) 322 if err != nil { 323 - log.Fatalln(err) 324 } 325 326 var result UpdateResult 327 if sourceURL.Scheme == "" { 328 file, err := os.Open(sourceURL.Path) 329 if err != nil { 330 - log.Fatalln(err) 331 } 332 defer file.Close() 333 ··· 346 } 347 348 webRoot := webRootArg(*updateSite) 349 - result = UpdateFromArchive(context.Background(), webRoot, contentType, file) 350 } else { 351 branch := "pages" 352 if sourceURL.Fragment != "" { ··· 354 } 355 356 webRoot := webRootArg(*updateSite) 357 - result = UpdateFromRepository(context.Background(), webRoot, sourceURL.String(), branch) 358 } 359 360 switch result.outcome { 361 case UpdateError: 362 - log.Printf("error: %s\n", result.err) 363 os.Exit(2) 364 case UpdateTimeout: 365 - log.Println("timeout") 366 os.Exit(1) 367 case UpdateCreated: 368 - log.Println("created") 369 case UpdateReplaced: 370 - log.Println("replaced") 371 case UpdateDeleted: 372 - log.Println("deleted") 373 case UpdateNoChange: 374 - log.Println("no-change") 375 } 376 377 case *freezeDomain != "" || *unfreezeDomain != "": ··· 386 } 387 388 if backend, err = CreateBackend(&config.Storage); err != nil { 389 - log.Fatalln(err) 390 } 391 392 - if err = backend.FreezeDomain(context.Background(), domain, freeze); err != nil { 393 - log.Fatalln(err) 394 } 395 if freeze { 396 log.Println("frozen") ··· 408 // The backend is not recreated (this is intentional as it allows preserving the cache). 409 OnReload(func() { 410 if newConfig, err := Configure(*configTomlPath); err != nil { 411 - log.Println("config: reload err:", err) 412 } else { 413 // From https://go.dev/ref/mem: 414 // > A read r of a memory location x holding a value that is not larger than ··· 418 // > concurrent write. 419 config = newConfig 420 if err = errors.Join( 421 - configureFeatures(), 422 - configureMemLimit(), 423 - configureWildcards(), 424 - configureFallback(), 425 ); err != nil { 426 // At this point the configuration is in an in-between, corrupted state, so 427 // the only reasonable choice is to crash. 428 - log.Fatalln("config: reload fail:", err) 429 } else { 430 - log.Println("config: reload ok") 431 } 432 } 433 }) ··· 436 // spends some time initializing (which the S3 backend does) a proxy like Caddy can race 437 // with git-pages on startup and return errors for requests that would have been served 438 // just 0.5s later. 439 - pagesListener := listen("pages", config.Server.Pages) 440 - caddyListener := listen("caddy", config.Server.Caddy) 441 - metricsListener := listen("metrics", config.Server.Metrics) 442 443 if backend, err = CreateBackend(&config.Storage); err != nil { 444 - log.Fatalln(err) 445 } 446 backend = NewObservedBackend(backend) 447 448 - go serve(pagesListener, ObserveHTTPHandler(http.HandlerFunc(ServePages))) 449 - go serve(caddyListener, ObserveHTTPHandler(http.HandlerFunc(ServeCaddy))) 450 - go serve(metricsListener, promhttp.Handler()) 451 452 if config.Insecure { 453 - log.Println("serve: ready (INSECURE)") 454 } else { 455 - log.Println("serve: ready") 456 } 457 458 WaitForInterrupt() 459 - log.Println("serve: exiting") 460 } 461 }
··· 27 var fallback http.Handler 28 var backend Backend 29 30 + func configureFeatures(ctx context.Context) (err error) { 31 if len(config.Features) > 0 { 32 + logc.Println(ctx, "features:", strings.Join(config.Features, ", ")) 33 } 34 return 35 } 36 37 + func configureMemLimit(ctx context.Context) (err error) { 38 // Avoid being OOM killed by not garbage collecting early enough. 39 memlimitBefore := datasize.ByteSize(debug.SetMemoryLimit(-1)) 40 automemlimit.SetGoMemLimitWithOpts( ··· 49 ) 50 memlimitAfter := datasize.ByteSize(debug.SetMemoryLimit(-1)) 51 if memlimitBefore == memlimitAfter { 52 + logc.Println(ctx, "memlimit: now", memlimitBefore.HR()) 53 } else { 54 + logc.Println(ctx, "memlimit: was", memlimitBefore.HR(), "now", memlimitAfter.HR()) 55 } 56 return 57 } 58 59 + func configureWildcards(_ context.Context) (err error) { 60 newWildcards, err := TranslateWildcards(config.Wildcard) 61 if err != nil { 62 return err ··· 66 } 67 } 68 69 + func configureFallback(_ context.Context) (err error) { 70 if config.Fallback.ProxyTo != "" { 71 var fallbackURL *url.URL 72 fallbackURL, err = url.Parse(config.Fallback.ProxyTo) ··· 91 return 92 } 93 94 + func listen(ctx context.Context, name string, listen string) net.Listener { 95 if listen == "-" { 96 return nil 97 } 98 99 protocol, address, ok := strings.Cut(listen, "/") 100 if !ok { 101 + logc.Fatalf(ctx, "%s: %s: malformed endpoint", name, listen) 102 } 103 104 listener, err := net.Listen(protocol, address) 105 if err != nil { 106 + logc.Fatalf(ctx, "%s: %s\n", name, err) 107 } 108 109 return listener ··· 125 }) 126 } 127 128 + func serve(ctx context.Context, listener net.Listener, handler http.Handler) { 129 if listener != nil { 130 handler = panicHandler(handler) 131 ··· 135 if config.Feature("serve-h2c") { 136 server.Protocols.SetUnencryptedHTTP2(true) 137 } 138 + logc.Fatalln(ctx, server.Serve(listener)) 139 } 140 } 141 ··· 146 case 1: 147 return arg 148 default: 149 + logc.Fatalln(context.Background(), 150 + "webroot argument must be either 'domain.tld' or 'domain.tld/dir") 151 return "" 152 } 153 } ··· 159 } else { 160 writer, err = os.Create(flag.Arg(0)) 161 if err != nil { 162 + logc.Fatalln(context.Background(), err) 163 } 164 } 165 return ··· 179 } 180 181 func Main() { 182 + ctx := context.Background() 183 + 184 flag.Usage = usage 185 printConfigEnvVars := flag.Bool("print-config-env-vars", false, 186 "print every recognized configuration environment variable and exit") ··· 229 cliOperations += 1 230 } 231 if cliOperations > 1 { 232 + logc.Fatalln(ctx, "-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive") 233 } 234 235 if *configTomlPath != "" && *noConfig { 236 + logc.Fatalln(ctx, "-no-config and -config are mutually exclusive") 237 } 238 239 if *printConfigEnvVars { ··· 246 *configTomlPath = "config.toml" 247 } 248 if config, err = Configure(*configTomlPath); err != nil { 249 + logc.Fatalln(ctx, "config:", err) 250 } 251 252 if *printConfig { ··· 258 defer FiniObservability() 259 260 if err = errors.Join( 261 + configureFeatures(ctx), 262 + configureMemLimit(ctx), 263 + configureWildcards(ctx), 264 + configureFallback(ctx), 265 ); err != nil { 266 + logc.Fatalln(ctx, err) 267 } 268 269 switch { 270 case *runMigration != "": 271 if backend, err = CreateBackend(&config.Storage); err != nil { 272 + logc.Fatalln(ctx, err) 273 } 274 275 + if err := RunMigration(ctx, *runMigration); err != nil { 276 + logc.Fatalln(ctx, err) 277 } 278 279 case *getBlob != "": 280 if backend, err = CreateBackend(&config.Storage); err != nil { 281 + logc.Fatalln(ctx, err) 282 } 283 284 + reader, _, _, err := backend.GetBlob(ctx, *getBlob) 285 if err != nil { 286 + logc.Fatalln(ctx, err) 287 } 288 io.Copy(fileOutputArg(), reader) 289 290 case *getManifest != "": 291 if backend, err = CreateBackend(&config.Storage); err != nil { 292 + logc.Fatalln(ctx, err) 293 } 294 295 webRoot := webRootArg(*getManifest) 296 + manifest, _, err := backend.GetManifest(ctx, webRoot, GetManifestOptions{}) 297 if err != nil { 298 + logc.Fatalln(ctx, err) 299 } 300 fmt.Fprintln(fileOutputArg(), ManifestDebugJSON(manifest)) 301 302 case *getArchive != "": 303 if backend, err = CreateBackend(&config.Storage); err != nil { 304 + logc.Fatalln(ctx, err) 305 } 306 307 webRoot := webRootArg(*getArchive) 308 manifest, manifestMtime, err := 309 + backend.GetManifest(ctx, webRoot, GetManifestOptions{}) 310 if err != nil { 311 + logc.Fatalln(ctx, err) 312 } 313 + CollectTar(ctx, fileOutputArg(), manifest, manifestMtime) 314 315 case *updateSite != "": 316 if backend, err = CreateBackend(&config.Storage); err != nil { 317 + logc.Fatalln(ctx, err) 318 } 319 320 if flag.NArg() != 1 { 321 + logc.Fatalln(ctx, "update source must be provided as the argument") 322 } 323 324 sourceURL, err := url.Parse(flag.Arg(0)) 325 if err != nil { 326 + logc.Fatalln(ctx, err) 327 } 328 329 var result UpdateResult 330 if sourceURL.Scheme == "" { 331 file, err := os.Open(sourceURL.Path) 332 if err != nil { 333 + logc.Fatalln(ctx, err) 334 } 335 defer file.Close() 336 ··· 349 } 350 351 webRoot := webRootArg(*updateSite) 352 + result = UpdateFromArchive(ctx, webRoot, contentType, file) 353 } else { 354 branch := "pages" 355 if sourceURL.Fragment != "" { ··· 357 } 358 359 webRoot := webRootArg(*updateSite) 360 + result = UpdateFromRepository(ctx, webRoot, sourceURL.String(), branch) 361 } 362 363 switch result.outcome { 364 case UpdateError: 365 + logc.Printf(ctx, "error: %s\n", result.err) 366 os.Exit(2) 367 case UpdateTimeout: 368 + logc.Println(ctx, "timeout") 369 os.Exit(1) 370 case UpdateCreated: 371 + logc.Println(ctx, "created") 372 case UpdateReplaced: 373 + logc.Println(ctx, "replaced") 374 case UpdateDeleted: 375 + logc.Println(ctx, "deleted") 376 case UpdateNoChange: 377 + logc.Println(ctx, "no-change") 378 } 379 380 case *freezeDomain != "" || *unfreezeDomain != "": ··· 389 } 390 391 if backend, err = CreateBackend(&config.Storage); err != nil { 392 + logc.Fatalln(ctx, err) 393 } 394 395 + if err = backend.FreezeDomain(ctx, domain, freeze); err != nil { 396 + logc.Fatalln(ctx, err) 397 } 398 if freeze { 399 log.Println("frozen") ··· 411 // The backend is not recreated (this is intentional as it allows preserving the cache). 412 OnReload(func() { 413 if newConfig, err := Configure(*configTomlPath); err != nil { 414 + logc.Println(ctx, "config: reload err:", err) 415 } else { 416 // From https://go.dev/ref/mem: 417 // > A read r of a memory location x holding a value that is not larger than ··· 421 // > concurrent write. 422 config = newConfig 423 if err = errors.Join( 424 + configureFeatures(ctx), 425 + configureMemLimit(ctx), 426 + configureWildcards(ctx), 427 + configureFallback(ctx), 428 ); err != nil { 429 // At this point the configuration is in an in-between, corrupted state, so 430 // the only reasonable choice is to crash. 431 + logc.Fatalln(ctx, "config: reload fail:", err) 432 } else { 433 + logc.Println(ctx, "config: reload ok") 434 } 435 } 436 }) ··· 439 // spends some time initializing (which the S3 backend does) a proxy like Caddy can race 440 // with git-pages on startup and return errors for requests that would have been served 441 // just 0.5s later. 442 + pagesListener := listen(ctx, "pages", config.Server.Pages) 443 + caddyListener := listen(ctx, "caddy", config.Server.Caddy) 444 + metricsListener := listen(ctx, "metrics", config.Server.Metrics) 445 446 if backend, err = CreateBackend(&config.Storage); err != nil { 447 + logc.Fatalln(ctx, err) 448 } 449 backend = NewObservedBackend(backend) 450 451 + go serve(ctx, pagesListener, ObserveHTTPHandler(http.HandlerFunc(ServePages))) 452 + go serve(ctx, caddyListener, ObserveHTTPHandler(http.HandlerFunc(ServeCaddy))) 453 + go serve(ctx, metricsListener, promhttp.Handler()) 454 455 if config.Insecure { 456 + logc.Println(ctx, "serve: ready (INSECURE)") 457 } else { 458 + logc.Println(ctx, "serve: ready") 459 } 460 461 WaitForInterrupt() 462 + logc.Println(ctx, "serve: exiting") 463 } 464 }