[mike@maginot]
This commit is contained in:
@@ -73,6 +73,38 @@ entweder die alte Datei oder die neue, nie eine halbe.
|
||||
Liegt das Binary irgendwo, wo der eigene Benutzer nicht schreiben darf
|
||||
(`/usr/local/bin`), sagt `dx --update` das und tut nichts — dann `sudo`.
|
||||
|
||||
### Einmal am Tag von selbst
|
||||
|
||||
Ohne Zutun sieht `dx` einmal am Tag nach und sagt danach am Ende der Ausgabe auf
|
||||
stderr Bescheid:
|
||||
|
||||
```
|
||||
dx 2.1.9 is available, run 'dx --update'
|
||||
```
|
||||
|
||||
Der Lauf im Vordergrund fasst dafür nie das Netz an. Er liest nur einen
|
||||
Merkzettel — `~/Library/Caches/dx/update.json`, unter Linux
|
||||
`~/.cache/dx/update.json` — und wenn der älter als einen Tag ist, startet er
|
||||
nebenbei `dx --update-refresh`: denselben Läufer noch einmal, abgekoppelt, ohne
|
||||
Ausgabe, nur zum Fragen. Auf dessen Antwort wartet niemand, sie steht beim
|
||||
nächsten Aufruf im Merkzettel. `dx` bleibt damit genau so schnell wie vorher,
|
||||
auch wenn der Server gerade schweigt.
|
||||
|
||||
Der Zeitstempel wandert weiter, *bevor* gefragt wird. Zwei gleichzeitige Läufe
|
||||
starten deshalb nur eine Abfrage, und ein Server, der nicht antwortet, wird
|
||||
morgen wieder gefragt und nicht bei jedem Aufruf. Lässt sich der Merkzettel
|
||||
nicht schreiben, unterbleibt die Frage ganz — sonst hinge an einem
|
||||
schreibgeschützten Cache-Verzeichnis ein Prozess je Aufruf.
|
||||
|
||||
Gefragt und gesagt wird nur, wenn stderr am Terminal hängt. In einer Pipe, in
|
||||
einem Skript und unter cron ist Ruhe, und `DX_NO_UPDATE_CHECK=1` schaltet es
|
||||
ganz ab.
|
||||
|
||||
Das kostet Platz: `net/http` samt TLS macht aus einem 1,8-MB-Binary eins von
|
||||
6,2 MB. Für ein Werkzeug, das ohnehin auf der Platte liegt, ist das der Preis
|
||||
dafür, sich selbst erneuern zu können — wer ihn nicht zahlen will, lässt
|
||||
`selfupdate.go` weg.
|
||||
|
||||
## Wiederverwenden
|
||||
|
||||
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der
|
||||
@@ -85,14 +117,27 @@ var selfUpdate = selfUpdater{
|
||||
asset: "foo",
|
||||
current: version,
|
||||
verify: []string{"--version"},
|
||||
every: 24 * time.Hour,
|
||||
quietEnv: "FOO_NO_UPDATE_CHECK",
|
||||
}
|
||||
```
|
||||
|
||||
— und `--update` und `--check-update` in die Optionen hängen. Alles Weitere in
|
||||
der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb nicht.
|
||||
`verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf hat; dann
|
||||
entfällt der Probelauf. Erwartet wird eine Gitea-Instanz (getestet mit 1.27);
|
||||
GitHub kennt dieselbe Route unter anderen Feldnamen und wird nicht bedient.
|
||||
Dazu drei Zeilen im Optionenteil — `--update`, `--check-update` und das
|
||||
versteckte `updateRefreshFlag` — und eine am Ende des Programms:
|
||||
|
||||
```go
|
||||
if hint := selfUpdate.daily(); hint != "" {
|
||||
fmt.Fprintln(os.Stderr, hint)
|
||||
}
|
||||
```
|
||||
|
||||
`daily` liefert nur den Text; wie er aussieht, entscheidet das Programm. Alles
|
||||
Weitere in der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb
|
||||
nicht. `verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf
|
||||
hat, dann entfällt der Probelauf; `every: 0` schaltet die Nachschau von selbst
|
||||
ab und lässt nur die beiden Optionen übrig. Erwartet wird eine Gitea-Instanz
|
||||
(getestet mit 1.27); GitHub kennt dieselbe Route unter anderen Feldnamen und
|
||||
wird nicht bedient.
|
||||
|
||||
## Optionen
|
||||
|
||||
|
||||
@@ -385,6 +385,9 @@ usage: dx [-n] [-d] [-r] [-t N] [path]
|
||||
--version
|
||||
--check-update look for a newer release
|
||||
--update download and install the newest release
|
||||
|
||||
dx looks for a new release once a day, in the background, and says so on
|
||||
stderr. DX_NO_UPDATE_CHECK=1 turns that off.
|
||||
`, version)
|
||||
}
|
||||
|
||||
@@ -426,6 +429,9 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
case a == updateRefreshFlag: // der Hintergrundlauf, nicht in der Hilfe
|
||||
selfUpdate.refresh()
|
||||
return
|
||||
case a == "--number":
|
||||
o.byCount = true
|
||||
case a == "--dirs":
|
||||
@@ -507,6 +513,11 @@ func main() {
|
||||
if res.unreadbl > 0 {
|
||||
fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl))
|
||||
}
|
||||
// Kostet nichts: der Hinweis stammt aus dem Merkzettel, gefragt wird
|
||||
// höchstens einmal am Tag und dann im Hintergrund.
|
||||
if hint := selfUpdate.daily(); hint != "" {
|
||||
fmt.Fprintln(os.Stderr, cDark.Sprint(hint))
|
||||
}
|
||||
}
|
||||
|
||||
func mustNum(s string) int {
|
||||
|
||||
+140
-4
@@ -38,6 +38,8 @@ var selfUpdate = selfUpdater{
|
||||
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 {
|
||||
@@ -45,6 +47,131 @@ type selfUpdater struct {
|
||||
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
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.1.8
|
||||
2.1.10
|
||||
|
||||
Reference in New Issue
Block a user