mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-06 09:00:19 +02:00
- The launcher keeps a log (/data/tailscale/launcher.log) and shows a notification when it cannot start. A payload manager does not show what a payload prints, so a failed start used to leave no trace. The Go runtime's stderr goes to the same file until the daemon opens its log. - New setting "who on the tailnet may connect": every device the tailnet's access rules allow (default), or only devices of the same user as the console. Applies to every forwarded TCP port, the UDP ports and the status page over the tailnet. - The status page shows when the console's key expires and warns from two weeks before; the console shows a notification at 14, 3 and 1 days. - A newer release is announced once on the console, not only on the page. - Status page: click an address to copy it; OS, status and address columns no longer break mid-word; the facts stack on narrow screens.
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The daemon asks GitHub now and then whether a newer release exists, so that
|
|
// the status page can say so. It never downloads or installs anything.
|
|
|
|
const releasesAPI = "https://api.github.com/repos/holdmysocks/ps5-tailscale/releases/latest"
|
|
|
|
type releaseInfo struct {
|
|
Version string // without the leading "v"
|
|
URL string
|
|
}
|
|
|
|
// parseVersion reads "v1.2.3" or "1.2.3-dev" as its three numbers.
|
|
func parseVersion(s string) (v [3]int, ok bool) {
|
|
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
|
|
if i := strings.IndexAny(s, "-+ "); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
parts := strings.Split(s, ".")
|
|
if len(parts) != 3 {
|
|
return v, false
|
|
}
|
|
for i, p := range parts {
|
|
n, err := strconv.Atoi(p)
|
|
if err != nil || n < 0 {
|
|
return v, false
|
|
}
|
|
v[i] = n
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// newerVersion reports whether latest is a later release than current.
|
|
func newerVersion(current, latest string) bool {
|
|
c, ok1 := parseVersion(current)
|
|
l, ok2 := parseVersion(latest)
|
|
if !ok1 || !ok2 {
|
|
return false
|
|
}
|
|
for i := range c {
|
|
if l[i] != c[i] {
|
|
return l[i] > c[i]
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fetchLatestRelease(ctx context.Context, url string) (releaseInfo, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return releaseInfo{}, err
|
|
}
|
|
req.Header.Set("User-Agent", "ps5-tailscale/"+version)
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return releaseInfo{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return releaseInfo{}, &httpStatusError{resp.Status}
|
|
}
|
|
var rel struct {
|
|
TagName string `json:"tag_name"`
|
|
HTMLURL string `json:"html_url"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
|
return releaseInfo{}, err
|
|
}
|
|
return releaseInfo{Version: strings.TrimPrefix(rel.TagName, "v"), URL: rel.HTMLURL}, nil
|
|
}
|
|
|
|
type httpStatusError struct{ status string }
|
|
|
|
func (e *httpStatusError) Error() string { return "unexpected response: " + e.status }
|
|
|
|
// watchForUpdates checks shortly after start and then twice a day, for as
|
|
// long as the setting is on.
|
|
func (d *daemon) watchForUpdates(ctx context.Context) {
|
|
timer := time.NewTimer(time.Minute)
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
timer.Reset(12 * time.Hour)
|
|
|
|
d.mu.Lock()
|
|
enabled := d.cfg.CheckUpdates
|
|
d.mu.Unlock()
|
|
if !enabled {
|
|
d.mu.Lock()
|
|
d.latest = releaseInfo{}
|
|
d.mu.Unlock()
|
|
continue
|
|
}
|
|
reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
rel, err := fetchLatestRelease(reqCtx, releasesAPI)
|
|
cancel()
|
|
if err != nil {
|
|
d.logf("update check: %v", err)
|
|
timer.Reset(time.Hour)
|
|
continue
|
|
}
|
|
d.mu.Lock()
|
|
known := d.latest.Version
|
|
d.latest = rel
|
|
d.mu.Unlock()
|
|
if d.debug != nil {
|
|
d.debug.Printf("update check: the latest release is %s", rel.Version)
|
|
}
|
|
if rel.Version != known && newerVersion(version, rel.Version) {
|
|
d.logf("a newer release is available: %s (running %s)", rel.Version, version)
|
|
d.notifyUpdate(rel.Version)
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateNotifiedFile remembers the release the user has been told about on
|
|
// screen, so that each release is announced once and not after every start.
|
|
const updateNotifiedFile = "update-notified"
|
|
|
|
func (d *daemon) notifyUpdate(latest string) {
|
|
path := filepath.Join(dataDir, updateNotifiedFile)
|
|
if b, err := os.ReadFile(path); err == nil && strings.TrimSpace(string(b)) == latest {
|
|
return
|
|
}
|
|
notify("Tailscale for PS5 %s is available (this is %s).\nSee %s", latest, version, d.webURL())
|
|
os.WriteFile(path, []byte(latest+"\n"), 0o644)
|
|
}
|