[mike@maginot]
This commit is contained in:
+148
-12
@@ -34,17 +34,144 @@ import (
|
||||
// ------------------------------------------------------------ Konfiguration
|
||||
|
||||
var selfUpdate = selfUpdater{
|
||||
repo: "https://git.micw.org/mike/dx",
|
||||
asset: "dx",
|
||||
current: version, // aus main.go, per -ldflags gesetzt
|
||||
verify: []string{"--version"},
|
||||
repo: "https://git.micw.org/mike/dx",
|
||||
asset: "dx",
|
||||
current: version, // aus main.go, per -ldflags gesetzt
|
||||
verify: []string{"--version"},
|
||||
every: 24 * time.Hour,
|
||||
quietEnv: "DX_NO_UPDATE_CHECK",
|
||||
}
|
||||
|
||||
type selfUpdater struct {
|
||||
repo string // Repo-URL wie im Browser: https://host/owner/repo
|
||||
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu
|
||||
current string // laufende Version
|
||||
verify []string // Probelauf des Downloads; leer lässt ihn ausfallen
|
||||
repo string // Repo-URL wie im Browser: https://host/owner/repo
|
||||
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu
|
||||
current string // laufende Version
|
||||
verify []string // Probelauf des Downloads; leer lässt ihn ausfallen
|
||||
every time.Duration // Abstand der Nachschau von selbst; 0 schaltet sie ab
|
||||
quietEnv string // diese Umgebungsvariable gesetzt: auch dann Ruhe
|
||||
}
|
||||
|
||||
// updateRefreshFlag ist die Option, mit der das Programm sich selbst im
|
||||
// Hintergrund aufruft. Sie steht bewusst nicht in der Hilfe.
|
||||
const updateRefreshFlag = "--update-refresh"
|
||||
|
||||
// ------------------------------------------------------- Nachschau von selbst
|
||||
|
||||
// daily ist der Anschluss für den gewöhnlichen Programmlauf. Sie kostet nichts:
|
||||
// im Vordergrund wird nie das Netz angefasst. Zurück kommt die Zeile, die auf
|
||||
// eine neue Version hinweist — oder "", wenn es nichts zu sagen gibt; wie sie
|
||||
// aussieht, entscheidet der Aufrufer. Ist der Merkzettel älter als `every`,
|
||||
// stößt sie nebenbei einen Hintergrundlauf an, dessen Antwort der nächste
|
||||
// Aufruf vorfindet.
|
||||
func (u selfUpdater) daily() string {
|
||||
if u.every <= 0 || os.Getenv(u.quietEnv) != "" || !updateOnTerminal() {
|
||||
return ""
|
||||
}
|
||||
st := u.loadState() // keine Datei: Nullwert, also sofort fällig
|
||||
|
||||
if time.Since(st.Checked) >= u.every {
|
||||
// Der Zeitstempel wandert weiter, bevor gefragt wird, nicht danach:
|
||||
// sonst starten zwei gleichzeitige Läufe zwei Abfragen, und ein Server,
|
||||
// der gerade nicht mag, bekäme bei jedem Aufruf eine neue. Bleibt der
|
||||
// Merkzettel nicht liegen, wird auch nicht gefragt — sonst hinge an
|
||||
// einem unbeschreibbaren Cache-Verzeichnis ein Prozess je Aufruf.
|
||||
st.Checked = time.Now()
|
||||
if u.saveState(st) == nil {
|
||||
u.spawnRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
if st.Latest == "" || updateCompare(st.Latest, u.current) <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s %s is available, run '%s --update'", u.asset, st.Latest, u.asset)
|
||||
}
|
||||
|
||||
// refresh ist der Hintergrundlauf: fragen, notieren, still bleiben. Das
|
||||
// Notieren erledigt latest, misslingt die Abfrage, bleibt der alte Stand.
|
||||
func (u selfUpdater) refresh() {
|
||||
_, _ = u.latest()
|
||||
}
|
||||
|
||||
// spawnRefresh ruft dieses Programm noch einmal auf, nur zum Fragen, und wartet
|
||||
// nicht. Ohne Wait wird das Kind beim Ende dieses Prozesses von init
|
||||
// übernommen — es lebt also länger als der Aufruf, und dessen Ausgabe bleibt
|
||||
// davon unberührt.
|
||||
func (u selfUpdater) spawnRefresh() {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(exe, updateRefreshFlag)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil // alles nach /dev/null
|
||||
if cmd.Start() == nil {
|
||||
cmd.Process.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// Der Hinweis gilt dem Menschen davor. Läuft das Programm in einer Pipe, in
|
||||
// einem Skript oder unter cron, wird weder gefragt noch etwas gesagt.
|
||||
func updateOnTerminal() bool {
|
||||
st, err := os.Stderr.Stat()
|
||||
return err == nil && st.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Merkzettel
|
||||
|
||||
// updateState ist das, was zwischen zwei Aufrufen übrig bleibt: wann zuletzt
|
||||
// gefragt wurde und was dabei herauskam.
|
||||
type updateState struct {
|
||||
Checked time.Time `json:"checked"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
// Der Merkzettel liegt im Cache-Verzeichnis, nicht in der Konfiguration: geht
|
||||
// er verloren, wird eben einmal zu früh gefragt.
|
||||
func (u selfUpdater) statePath() (string, error) {
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, u.asset, "update.json"), nil
|
||||
}
|
||||
|
||||
func (u selfUpdater) loadState() updateState {
|
||||
var st updateState
|
||||
path, err := u.statePath()
|
||||
if err != nil {
|
||||
return st
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return st
|
||||
}
|
||||
json.Unmarshal(b, &st) // eine kaputte Datei zählt wie keine
|
||||
return st
|
||||
}
|
||||
|
||||
func (u selfUpdater) saveState(st updateState) error {
|
||||
path, err := u.statePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Über eine Nebendatei, damit ein gleichzeitiger Lauf nie ein halbes JSON
|
||||
// vorfindet.
|
||||
tmp := path + ".new"
|
||||
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Ablauf
|
||||
@@ -139,7 +266,7 @@ func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (str
|
||||
}
|
||||
tmp := f.Name()
|
||||
|
||||
resp, err := updateGet(a.URL)
|
||||
resp, err := updateGet(context.Background(), a.URL)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmp)
|
||||
@@ -225,7 +352,12 @@ func (u selfUpdater) latest() (updateRelease, error) {
|
||||
if err != nil {
|
||||
return updateRelease{}, err
|
||||
}
|
||||
resp, err := updateGet(base + "/releases/latest")
|
||||
// Die Frage ist klein; hängt sie, hängt sie nicht lange. Das großzügige
|
||||
// Zeitlimit von updateClient gilt dem Download.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := updateGet(ctx, base+"/releases/latest")
|
||||
if err != nil {
|
||||
return updateRelease{}, err
|
||||
}
|
||||
@@ -238,6 +370,10 @@ func (u selfUpdater) latest() (updateRelease, error) {
|
||||
if rel.TagName == "" {
|
||||
return updateRelease{}, fmt.Errorf("%s has no releases", u.repo)
|
||||
}
|
||||
|
||||
// Jede geglückte Frage füllt den Merkzettel — egal ob sie von --update,
|
||||
// --check-update oder dem Hintergrundlauf kam.
|
||||
u.saveState(updateState{Checked: time.Now(), Latest: rel.TagName})
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
@@ -261,8 +397,8 @@ func (u selfUpdater) apiBase() (string, error) {
|
||||
// aber nicht ewig.
|
||||
var updateClient = &http.Client{Timeout: 5 * time.Minute}
|
||||
|
||||
func updateGet(target string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, target, nil)
|
||||
func updateGet(ctx context.Context, target string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user