@ 261101.7
current
package main

import (
	"fmt"
	"net/http"
	"net/netip"
	"os"
	"os/exec"
	"path/filepath"
	"time"
)

// about describes the server for its index: where it listens, since when,
// and the binary it runs, so the index can tell when a rebuild has left it
// behind. Tests leave it zero.
type about struct {
	ep      netip.AddrPort
	started time.Time
	exe     string
	exeInfo os.FileInfo
}

func newAbout(ep netip.AddrPort) about {
	a := about{ep: ep, started: time.Now()}
	if exe, err := os.Executable(); err == nil {
		if info, err := os.Stat(exe); err == nil {
			a.exe, a.exeInfo = exe, info
		}
	}
	return a
}

// stale reports whether the binary on disk is no longer the one running.
func (a about) stale() bool {
	if a.exeInfo == nil {
		return false
	}
	info, err := os.Stat(a.exe)
	return err != nil || !os.SameFile(info, a.exeInfo) || !info.ModTime().Equal(a.exeInfo.ModTime())
}

// restartCmd is the command that moves this server's trees to the new
// binary; with no address it moves every server its binary started. It
// names the binary by its path when the name on PATH leads to another,
// eg the package's, which would leave this server alone.
func (a about) restartCmd() string {
	if a.exe != "" {
		p, err := exec.LookPath(progName)
		if err == nil {
			p, err = filepath.EvalSymlinks(p)
		}
		if err != nil || p != a.exe {
			return tildePath(a.exe) + " --restart"
		}
	}
	return progName + " --restart"
}

// status is the index header's account of the server's age. The build
// time is on every page, above the header.
func (a about) status() string {
	if a.started.IsZero() {
		return ""
	}
	return "up since " + clock(a.started)
}

// built is when the running binary was written, read once at start, so
// each page can say which build served it. debugOn, set by --debug,
// puts it above the header, and on the index, each server's build and
// start and when each lock was written. Without it they stay hidden, as
// they serve debugging alone.
var debugOn bool

var buildCommit = buildRev()

var built = func() time.Time {
	exe, err := os.Executable()
	if err != nil {
		return time.Time{}
	}
	info, err := os.Stat(exe)
	if err != nil {
		return time.Time{}
	}
	return info.ModTime()
}()

// versionLine is --version's account of the build, in the words of the
// stamp, with the full date since a build may be old.
func versionLine(t time.Time, commit string) string {
	when := "at an unknown time"
	if !t.IsZero() {
		when = t.Format("2006-01-02 15:04")
	}
	return progName + " built " + when + ", from " + commit
}

// stamp is the build as a page shows it: the time of day, the seconds
// since, which live.js counts on from Millis, the commit, and how long
// the page took to render.
type stamp struct {
	Clock    string
	Millis   int64
	Ago      int
	Commit   string
	Rendered string
}

type startKey struct{}

// rendered says how long the request has taken so far, in milliseconds
// with a decimal below 10, since most docs render in under one.
func rendered(r *http.Request) string {
	start, ok := r.Context().Value(startKey{}).(time.Time)
	if !ok {
		return ""
	}
	ms := float64(time.Since(start).Microseconds()) / 1000
	if ms < 10 {
		return fmt.Sprintf("%.1f ms", ms)
	}
	return fmt.Sprintf("%.0f ms", ms)
}

// clock gives the time of day, with the date when it is not today.
func clock(t time.Time) string {
	if t.IsZero() {
		return ""
	}
	if y, m, d := t.Date(); y == time.Now().Year() && m == time.Now().Month() && d == time.Now().Day() {
		return t.Format("15:04")
	}
	return t.Format("Jan 2 15:04")
}