[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
|
Liegt das Binary irgendwo, wo der eigene Benutzer nicht schreiben darf
|
||||||
(`/usr/local/bin`), sagt `dx --update` das und tut nichts — dann `sudo`.
|
(`/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
|
## Wiederverwenden
|
||||||
|
|
||||||
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der
|
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der
|
||||||
@@ -81,18 +113,31 @@ oben anpassen —
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
var selfUpdate = selfUpdater{
|
var selfUpdate = selfUpdater{
|
||||||
repo: "https://git.micw.org/mike/foo",
|
repo: "https://git.micw.org/mike/foo",
|
||||||
asset: "foo",
|
asset: "foo",
|
||||||
current: version,
|
current: version,
|
||||||
verify: []string{"--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
|
Dazu drei Zeilen im Optionenteil — `--update`, `--check-update` und das
|
||||||
der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb nicht.
|
versteckte `updateRefreshFlag` — und eine am Ende des Programms:
|
||||||
`verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf hat; dann
|
|
||||||
entfällt der Probelauf. Erwartet wird eine Gitea-Instanz (getestet mit 1.27);
|
```go
|
||||||
GitHub kennt dieselbe Route unter anderen Feldnamen und wird nicht bedient.
|
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
|
## Optionen
|
||||||
|
|
||||||
|
|||||||
@@ -385,6 +385,9 @@ usage: dx [-n] [-d] [-r] [-t N] [path]
|
|||||||
--version
|
--version
|
||||||
--check-update look for a newer release
|
--check-update look for a newer release
|
||||||
--update download and install the newest 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)
|
`, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +429,9 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
case a == updateRefreshFlag: // der Hintergrundlauf, nicht in der Hilfe
|
||||||
|
selfUpdate.refresh()
|
||||||
|
return
|
||||||
case a == "--number":
|
case a == "--number":
|
||||||
o.byCount = true
|
o.byCount = true
|
||||||
case a == "--dirs":
|
case a == "--dirs":
|
||||||
@@ -507,6 +513,11 @@ func main() {
|
|||||||
if res.unreadbl > 0 {
|
if res.unreadbl > 0 {
|
||||||
fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl))
|
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 {
|
func mustNum(s string) int {
|
||||||
|
|||||||
+148
-12
@@ -34,17 +34,144 @@ import (
|
|||||||
// ------------------------------------------------------------ Konfiguration
|
// ------------------------------------------------------------ Konfiguration
|
||||||
|
|
||||||
var selfUpdate = selfUpdater{
|
var selfUpdate = selfUpdater{
|
||||||
repo: "https://git.micw.org/mike/dx",
|
repo: "https://git.micw.org/mike/dx",
|
||||||
asset: "dx",
|
asset: "dx",
|
||||||
current: version, // aus main.go, per -ldflags gesetzt
|
current: version, // aus main.go, per -ldflags gesetzt
|
||||||
verify: []string{"--version"},
|
verify: []string{"--version"},
|
||||||
|
every: 24 * time.Hour,
|
||||||
|
quietEnv: "DX_NO_UPDATE_CHECK",
|
||||||
}
|
}
|
||||||
|
|
||||||
type selfUpdater struct {
|
type selfUpdater struct {
|
||||||
repo string // Repo-URL wie im Browser: https://host/owner/repo
|
repo string // Repo-URL wie im Browser: https://host/owner/repo
|
||||||
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu
|
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu
|
||||||
current string // laufende Version
|
current string // laufende Version
|
||||||
verify []string // Probelauf des Downloads; leer lässt ihn ausfallen
|
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
|
// ------------------------------------------------------------------ Ablauf
|
||||||
@@ -139,7 +266,7 @@ func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (str
|
|||||||
}
|
}
|
||||||
tmp := f.Name()
|
tmp := f.Name()
|
||||||
|
|
||||||
resp, err := updateGet(a.URL)
|
resp, err := updateGet(context.Background(), a.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
os.Remove(tmp)
|
os.Remove(tmp)
|
||||||
@@ -225,7 +352,12 @@ func (u selfUpdater) latest() (updateRelease, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return updateRelease{}, err
|
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 {
|
if err != nil {
|
||||||
return updateRelease{}, err
|
return updateRelease{}, err
|
||||||
}
|
}
|
||||||
@@ -238,6 +370,10 @@ func (u selfUpdater) latest() (updateRelease, error) {
|
|||||||
if rel.TagName == "" {
|
if rel.TagName == "" {
|
||||||
return updateRelease{}, fmt.Errorf("%s has no releases", u.repo)
|
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
|
return rel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +397,8 @@ func (u selfUpdater) apiBase() (string, error) {
|
|||||||
// aber nicht ewig.
|
// aber nicht ewig.
|
||||||
var updateClient = &http.Client{Timeout: 5 * time.Minute}
|
var updateClient = &http.Client{Timeout: 5 * time.Minute}
|
||||||
|
|
||||||
func updateGet(target string) (*http.Response, error) {
|
func updateGet(ctx context.Context, target string) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, target, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.1.8
|
2.1.10
|
||||||
|
|||||||
Reference in New Issue
Block a user