From 0d67aa251d18f8ab47cfd90072e9d0387dff4224 Mon Sep 17 00:00:00 2001 From: Ainar Garipov Date: Wed, 13 Jan 2021 16:18:51 +0300 Subject: [PATCH] Pull request: 2546 updater fix Merge in DNS/adguard-home from 2546-updater-fix to master Closes #2546. Squashed commit of the following: commit af243c9fad710efe099506fda281e628c3e5ec30 Author: Ainar Garipov Date: Wed Jan 13 14:33:37 2021 +0300 updater: fix go 1.14 compat commit 742fba24b300ce51c04acb586996c3c75e56ea20 Author: Ainar Garipov Date: Wed Jan 13 13:58:27 2021 +0300 util: imp error check commit c2bdbce8af657a7f4b7e05c018cfacba86e06753 Author: Ainar Garipov Date: Mon Jan 11 18:51:26 2021 +0300 all: fix and refactor update checking --- HACKING.md | 8 + internal/home/config.go | 3 +- internal/home/control.go | 5 +- internal/home/controlupdate.go | 16 +- internal/home/home.go | 63 +--- internal/home/options.go | 4 +- internal/update/check.go | 114 ------- internal/update/update_test.go | 215 ------------ internal/updater/check.go | 117 +++++++ .../testdata}/AdGuardHome.tar.gz | Bin .../test => updater/testdata}/AdGuardHome.zip | Bin internal/{update => updater}/updater.go | 178 ++++++---- internal/updater/updater_test.go | 316 ++++++++++++++++++ internal/util/helpers.go | 2 +- internal/version/version.go | 53 +++ main.go | 14 +- scripts/make/build-release.sh | 11 + scripts/make/go-build.sh | 15 +- 18 files changed, 669 insertions(+), 465 deletions(-) delete mode 100644 internal/update/check.go delete mode 100644 internal/update/update_test.go create mode 100644 internal/updater/check.go rename internal/{update/test => updater/testdata}/AdGuardHome.tar.gz (100%) rename internal/{update/test => updater/testdata}/AdGuardHome.zip (100%) rename internal/{update => updater}/updater.go (70%) create mode 100644 internal/updater/updater_test.go create mode 100644 internal/version/version.go diff --git a/HACKING.md b/HACKING.md index abfa86e8..dc291bb7 100644 --- a/HACKING.md +++ b/HACKING.md @@ -46,6 +46,14 @@ The rules are mostly sorted in the alphabetical order. * Avoid `new`, especially with structs. + * Check against empty strings like this: + + ```go + if s == "" { + // … + } + ``` + * Constructors should validate their arguments and return meaningful errors. As a corollary, avoid lazy initialization. diff --git a/internal/home/config.go b/internal/home/config.go index 840678d4..f7b799dc 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -11,6 +11,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/stats" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" @@ -177,7 +178,7 @@ func initConfig() { config.DHCP.Conf4.ICMPTimeout = 1000 config.DHCP.Conf6.LeaseDuration = 86400 - if updateChannel == "none" || updateChannel == "edge" || updateChannel == "development" { + if ch := version.Channel(); ch == "edge" || ch == "development" { config.BetaBindPort = 3001 } } diff --git a/internal/home/control.go b/internal/home/control.go index 0bdd8aab..01bf00ce 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" "github.com/NYTimes/gziphandler" ) @@ -53,7 +54,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { "http_port": config.BindPort, "dns_port": config.DNS.Port, "running": isRunning(), - "version": versionString, + "version": version.Version(), "language": config.Language, "protection_enabled": c.ProtectionEnabled, @@ -118,7 +119,7 @@ func registerControlHandlers() { } func httpRegister(method, url string, handler func(http.ResponseWriter, *http.Request)) { - if len(method) == 0 { + if method == "" { // "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method Context.mux.HandleFunc(url, postInstall(handler)) return diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go index ff9dc4ab..a502b902 100644 --- a/internal/home/controlupdate.go +++ b/internal/home/controlupdate.go @@ -12,7 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" - "github.com/AdguardTeam/AdGuardHome/internal/update" + "github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/golibs/log" ) @@ -47,13 +47,13 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { } } - var info update.VersionInfo + var info updater.VersionInfo for i := 0; i != 3; i++ { func() { Context.controlLock.Lock() defer Context.controlLock.Unlock() - info, err = Context.updater.GetVersionResponse(req.RecheckNow) + info, err = Context.updater.VersionInfo(req.RecheckNow) }() if err != nil { @@ -75,7 +75,9 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { break } if err != nil { - httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err) + vcu := Context.updater.VersionCheckURL() + httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", vcu, err, err) + return } @@ -88,12 +90,12 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { // Perform an update procedure to the latest available version func handleUpdate(w http.ResponseWriter, _ *http.Request) { - if len(Context.updater.NewVersion) == 0 { + if Context.updater.NewVersion() == "" { httpError(w, http.StatusBadRequest, "/update request isn't allowed now") return } - err := Context.updater.DoUpdate() + err := Context.updater.Update() if err != nil { httpError(w, http.StatusInternalServerError, "%s", err) return @@ -108,7 +110,7 @@ func handleUpdate(w http.ResponseWriter, _ *http.Request) { } // Convert version.json data to our JSON response -func getVersionResp(info update.VersionInfo) []byte { +func getVersionResp(info updater.VersionInfo) []byte { ret := make(map[string]interface{}) ret["can_autoupdate"] = false ret["new_version"] = info.NewVersion diff --git a/internal/home/home.go b/internal/home/home.go index aeb875a4..fbead57a 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -27,8 +27,9 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" - "github.com/AdguardTeam/AdGuardHome/internal/update" + "github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" "gopkg.in/natefinch/lumberjack.v2" ) @@ -38,15 +39,6 @@ const ( configSyslog = "syslog" ) -// Update-related variables -var ( - versionString = "dev" - updateChannel = "none" - versionCheckURL = "" - ARMVersion = "" - MIPSVersion = "" -) - // Global context type homeContext struct { // Modules @@ -65,7 +57,7 @@ type homeContext struct { web *Web // Web (HTTP, HTTPS) module tls *TLSMod // TLS module autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files - updater *update.Updater + updater *updater.Updater ipDetector *ipDetector @@ -99,14 +91,7 @@ func (c *homeContext) getDataDir() string { var Context homeContext // Main is the entry point -func Main(version, channel, armVer, mipsVer string) { - // Init update-related global variables - versionString = version - updateChannel = channel - ARMVersion = armVer - MIPSVersion = mipsVer - versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json" - +func Main() { // config can be specified, which reads options from there, but other command line flags have to override config values // therefore, we must do it manually instead of using a lib args := loadOptions() @@ -139,20 +124,6 @@ func Main(version, channel, armVer, mipsVer string) { run(args) } -// version - returns the current version string -func version() string { - // TODO(a.garipov): I'm pretty sure we can extract some of this stuff - // from the build info. - msg := "AdGuard Home, version %s, channel %s, arch %s %s" - if ARMVersion != "" { - msg = msg + " v" + ARMVersion - } else if MIPSVersion != "" { - msg = msg + " " + MIPSVersion - } - - return fmt.Sprintf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH) -} - func setupContext(args options) { Context.runningAsService = args.runningAsService Context.disableUpdate = args.disableUpdate @@ -214,15 +185,16 @@ func setupConfig(args options) { Context.autoHosts.Init("") - Context.updater = update.NewUpdater(update.Config{ - Client: Context.client, - WorkDir: Context.workDir, - VersionURL: versionCheckURL, - VersionString: versionString, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - ARMVersion: ARMVersion, - ConfigName: config.getConfigFilename(), + Context.updater = updater.NewUpdater(&updater.Config{ + Client: Context.client, + Version: version.Version(), + Channel: version.Channel(), + GOARCH: runtime.GOARCH, + GOOS: runtime.GOOS, + GOARM: version.GOARM(), + GOMIPS: version.GOMIPS(), + WorkDir: Context.workDir, + ConfName: config.getConfigFilename(), }) Context.clients.Init(config.Clients, Context.dhcpServer, &Context.autoHosts) @@ -260,7 +232,7 @@ func run(args options) { memoryUsage(args) // print the first message after logger is configured - log.Println(version()) + log.Println(version.Full()) log.Debug("Current working directory is %s", Context.workDir) if args.runningAsService { log.Info("AdGuard Home is running as a service") @@ -690,10 +662,11 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err return nil, agherr.Many(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...) } -func getHTTPProxy(req *http.Request) (*url.URL, error) { - if len(config.ProxyURL) == 0 { +func getHTTPProxy(_ *http.Request) (*url.URL, error) { + if config.ProxyURL == "" { return nil, nil } + return url.Parse(config.ProxyURL) } diff --git a/internal/home/options.go b/internal/home/options.go index 514ed3a1..0493e856 100644 --- a/internal/home/options.go +++ b/internal/home/options.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "strconv" + + "github.com/AdguardTeam/AdGuardHome/internal/version" ) // options passed from command-line arguments @@ -180,7 +182,7 @@ var versionArg = arg{ "Show the version and exit", "version", "", nil, nil, func(o options, exec string) (effect, error) { - return func() error { fmt.Println(version()); os.Exit(0); return nil }, nil + return func() error { fmt.Println(version.Full()); os.Exit(0); return nil }, nil }, func(o options) []string { return nil }, } diff --git a/internal/update/check.go b/internal/update/check.go deleted file mode 100644 index e83ab5c2..00000000 --- a/internal/update/check.go +++ /dev/null @@ -1,114 +0,0 @@ -package update - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "strings" - "time" - - "github.com/AdguardTeam/AdGuardHome/internal/aghio" -) - -const versionCheckPeriod = 8 * 60 * 60 - -// VersionInfo - VersionInfo -type VersionInfo struct { - NewVersion string // New version string - Announcement string // Announcement text - AnnouncementURL string // Announcement URL - SelfUpdateMinVersion string // Min version starting with which we can auto-update - CanAutoUpdate bool // If true - we can auto-update -} - -// MaxResponseSize is responses on server's requests maximum length in bytes. -const MaxResponseSize = 64 * 1024 - -// GetVersionResponse - downloads version.json (if needed) and deserializes it -func (u *Updater) GetVersionResponse(forceRecheck bool) (VersionInfo, error) { - if !forceRecheck && - u.versionCheckLastTime.Unix()+versionCheckPeriod > time.Now().Unix() { - return u.parseVersionResponse(u.versionJSON) - } - - resp, err := u.Client.Get(u.VersionURL) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) - } - defer resp.Body.Close() - - resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxResponseSize) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: LimitReadCloser: %w", err) - } - defer resp.Body.Close() - - // This use of ReadAll is safe, because we just limited the appropriate - // ReadCloser. - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) - } - - u.versionJSON = body - u.versionCheckLastTime = time.Now() - - return u.parseVersionResponse(body) -} - -func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { - info := VersionInfo{} - versionJSON := make(map[string]interface{}) - err := json.Unmarshal(data, &versionJSON) - if err != nil { - return info, fmt.Errorf("version.json: %w", err) - } - - var ok1, ok2, ok3, ok4 bool - info.NewVersion, ok1 = versionJSON["version"].(string) - info.Announcement, ok2 = versionJSON["announcement"].(string) - info.AnnouncementURL, ok3 = versionJSON["announcement_url"].(string) - info.SelfUpdateMinVersion, ok4 = versionJSON["selfupdate_min_version"].(string) - if !ok1 || !ok2 || !ok3 || !ok4 { - return info, fmt.Errorf("version.json: invalid data") - } - - packageURL, ok := u.getDownloadURL(versionJSON) - - if ok && - info.NewVersion != u.VersionString && - strings.TrimPrefix(u.VersionString, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { - info.CanAutoUpdate = true - } - - u.NewVersion = info.NewVersion - u.PackageURL = packageURL - - return info, nil -} - -// Get download URL for the current GOOS/GOARCH/ARMVersion -func (u *Updater) getDownloadURL(json map[string]interface{}) (string, bool) { - var key string - - if u.Arch == "arm" && u.ARMVersion != "" { - // the key is: - // download_linux_armv5 for ARMv5 - // download_linux_armv6 for ARMv6 - // download_linux_armv7 for ARMv7 - key = fmt.Sprintf("download_%s_%sv%s", u.OS, u.Arch, u.ARMVersion) - } - - val, ok := json[key] - if !ok { - // the key is download_linux_arm or download_linux_arm64 for regular ARM versions - key = fmt.Sprintf("download_%s_%s", u.OS, u.Arch) - val, ok = json[key] - } - - if !ok { - return "", false - } - - return val.(string), true -} diff --git a/internal/update/update_test.go b/internal/update/update_test.go deleted file mode 100644 index ceca2b9d..00000000 --- a/internal/update/update_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package update - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "testing" - - "github.com/AdguardTeam/AdGuardHome/internal/testutil" - "github.com/stretchr/testify/assert" -) - -func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) -} - -func startHTTPServer(data string) (net.Listener, uint16) { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(data)) - }) - - listener, err := net.Listen("tcp", ":0") - if err != nil { - panic(err) - } - - go func() { _ = http.Serve(listener, mux) }() - return listener, uint16(listener.Addr().(*net.TCPAddr).Port) -} - -func TestUpdateGetVersion(t *testing.T) { - const jsonData = `{ - "version": "v0.103.0-beta2", - "announcement": "AdGuard Home v0.103.0-beta2 is now available!", - "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", - "selfupdate_min_version": "v0.0", - "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", - "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", - "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", - "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", - "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", - "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", - "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", - "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", - "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", - "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", - "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", - "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", - "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", - "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", - "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", - "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", - "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", - "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" -}` - - l, lport := startHTTPServer(jsonData) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - Client: &http.Client{}, - VersionURL: fmt.Sprintf("http://127.0.0.1:%d/", lport), - OS: "linux", - Arch: "arm", - VersionString: "v0.103.0-beta1", - }) - - info, err := u.GetVersionResponse(false) - assert.Nil(t, err) - assert.Equal(t, "v0.103.0-beta2", info.NewVersion) - assert.Equal(t, "AdGuard Home v0.103.0-beta2 is now available!", info.Announcement) - assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) - assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) - assert.True(t, info.CanAutoUpdate) - - _ = l.Close() - - // check cached - _, err = u.GetVersionResponse(false) - assert.Nil(t, err) -} - -func TestUpdate(t *testing.T) { - _ = os.Mkdir("aghtest", 0o755) - defer func() { - _ = os.RemoveAll("aghtest") - }() - - // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome", []byte("AdGuardHome"), 0o755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) - - // start server for returning package file - pkgData, err := ioutil.ReadFile("test/AdGuardHome.tar.gz") - assert.Nil(t, err) - l, lport := startHTTPServer(string(pkgData)) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - Client: &http.Client{}, - PackageURL: fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.tar.gz", lport), - VersionString: "v0.103.0", - NewVersion: "v0.103.1", - ConfigName: "aghtest/AdGuardHome.yaml", - WorkDir: "aghtest", - }) - - assert.Nil(t, u.prepare()) - u.currentExeName = "aghtest/AdGuardHome" - assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) - assert.Nil(t, u.unpack()) - // assert.Nil(t, u.check()) - assert.Nil(t, u.backup()) - assert.Nil(t, u.replace()) - u.clean() - - // check backup files - d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) - - d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome", string(d)) - - // check updated files - d, err = ioutil.ReadFile("aghtest/AdGuardHome") - assert.Nil(t, err) - assert.Equal(t, "1", string(d)) - - d, err = ioutil.ReadFile("aghtest/README.md") - assert.Nil(t, err) - assert.Equal(t, "2", string(d)) - - d, err = ioutil.ReadFile("aghtest/LICENSE.txt") - assert.Nil(t, err) - assert.Equal(t, "3", string(d)) - - d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) -} - -func TestUpdateWindows(t *testing.T) { - _ = os.Mkdir("aghtest", 0o755) - defer func() { - _ = os.RemoveAll("aghtest") - }() - - // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.exe", []byte("AdGuardHome.exe"), 0o755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) - - // start server for returning package file - pkgData, err := ioutil.ReadFile("test/AdGuardHome.zip") - assert.Nil(t, err) - l, lport := startHTTPServer(string(pkgData)) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - WorkDir: "aghtest", - Client: &http.Client{}, - PackageURL: fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.zip", lport), - OS: "windows", - VersionString: "v0.103.0", - NewVersion: "v0.103.1", - ConfigName: "aghtest/AdGuardHome.yaml", - }) - - assert.Nil(t, u.prepare()) - u.currentExeName = "aghtest/AdGuardHome.exe" - assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) - assert.Nil(t, u.unpack()) - // assert.Nil(t, u.check()) - assert.Nil(t, u.backup()) - assert.Nil(t, u.replace()) - u.clean() - - // check backup files - d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) - - d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.exe") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.exe", string(d)) - - // check updated files - d, err = ioutil.ReadFile("aghtest/AdGuardHome.exe") - assert.Nil(t, err) - assert.Equal(t, "1", string(d)) - - d, err = ioutil.ReadFile("aghtest/README.md") - assert.Nil(t, err) - assert.Equal(t, "2", string(d)) - - d, err = ioutil.ReadFile("aghtest/LICENSE.txt") - assert.Nil(t, err) - assert.Equal(t, "3", string(d)) - - d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) -} diff --git a/internal/updater/check.go b/internal/updater/check.go new file mode 100644 index 00000000..6418ffb6 --- /dev/null +++ b/internal/updater/check.go @@ -0,0 +1,117 @@ +package updater + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" +) + +// TODO(a.garipov): Make configurable. +const versionCheckPeriod = 8 * time.Hour + +// VersionInfo contains information about a new version. +type VersionInfo struct { + NewVersion string + Announcement string + AnnouncementURL string + SelfUpdateMinVersion string + CanAutoUpdate bool +} + +// MaxResponseSize is responses on server's requests maximum length in bytes. +const MaxResponseSize = 64 * 1024 + +// VersionInfo downloads the latest version information. If forceRecheck is +// false and there are cached results, those results are returned. +func (u *Updater) VersionInfo(forceRecheck bool) (VersionInfo, error) { + u.mu.Lock() + defer u.mu.Unlock() + + now := time.Now() + recheckTime := u.prevCheckTime.Add(versionCheckPeriod) + if !forceRecheck && now.Before(recheckTime) { + return u.prevCheckResult, u.prevCheckError + } + + vcu := u.versionCheckURL + resp, err := u.client.Get(vcu) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err) + } + defer resp.Body.Close() + + resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxResponseSize) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: LimitReadCloser: %w", err) + } + defer resp.Body.Close() + + // This use of ReadAll is safe, because we just limited the appropriate + // ReadCloser. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err) + } + + u.prevCheckTime = time.Now() + u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body) + + return u.prevCheckResult, u.prevCheckError +} + +func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { + info := VersionInfo{} + versionJSON := make(map[string]interface{}) + err := json.Unmarshal(data, &versionJSON) + if err != nil { + return info, fmt.Errorf("version.json: %w", err) + } + + var ok1, ok2, ok3, ok4 bool + info.NewVersion, ok1 = versionJSON["version"].(string) + info.Announcement, ok2 = versionJSON["announcement"].(string) + info.AnnouncementURL, ok3 = versionJSON["announcement_url"].(string) + info.SelfUpdateMinVersion, ok4 = versionJSON["selfupdate_min_version"].(string) + if !ok1 || !ok2 || !ok3 || !ok4 { + return info, fmt.Errorf("version.json: invalid data") + } + + packageURL, ok := u.downloadURL(versionJSON) + if ok && + info.NewVersion != u.version && + strings.TrimPrefix(u.version, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { + info.CanAutoUpdate = true + } + + u.newVersion = info.NewVersion + u.packageURL = packageURL + + return info, nil +} + +// downloadURL returns the download URL for current build. +func (u *Updater) downloadURL(json map[string]interface{}) (string, bool) { + var key string + + if u.goarch == "arm" && u.goarm != "" { + key = fmt.Sprintf("download_%s_%sv%s", u.goos, u.goarch, u.goarm) + } else if u.goarch == "mips" && u.gomips != "" { + key = fmt.Sprintf("download_%s_%s_%s", u.goos, u.goarch, u.gomips) + } + + val, ok := json[key] + if !ok { + key = fmt.Sprintf("download_%s_%s", u.goos, u.goarch) + val, ok = json[key] + } + + if !ok { + return "", false + } + + return val.(string), true +} diff --git a/internal/update/test/AdGuardHome.tar.gz b/internal/updater/testdata/AdGuardHome.tar.gz similarity index 100% rename from internal/update/test/AdGuardHome.tar.gz rename to internal/updater/testdata/AdGuardHome.tar.gz diff --git a/internal/update/test/AdGuardHome.zip b/internal/updater/testdata/AdGuardHome.zip similarity index 100% rename from internal/update/test/AdGuardHome.zip rename to internal/updater/testdata/AdGuardHome.zip diff --git a/internal/update/updater.go b/internal/updater/updater.go similarity index 70% rename from internal/update/updater.go rename to internal/updater/updater.go index b6901889..8d0bee5f 100644 --- a/internal/update/updater.go +++ b/internal/updater/updater.go @@ -1,5 +1,5 @@ -// Package update provides an updater for AdGuardHome. -package update +// Package updater provides an updater for AdGuardHome. +package updater import ( "archive/tar" @@ -9,62 +9,106 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "os/exec" + "path" "path/filepath" "strings" + "sync" "time" "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" ) -// Updater - Updater +// Updater is the AdGuard Home updater. type Updater struct { - Config // Updater configuration + client *http.Client + version string + channel string + goarch string + goos string + goarm string + gomips string + + workDir string + confName string + versionCheckURL string + + // mu protects all fields below. + mu *sync.RWMutex + + // TODO(a.garipov): See if all of these fields actually have to be in + // this struct. currentExeName string // current binary executable - updateDir string // "work_dir/agh-update-v0.103.0" - packageName string // "work_dir/agh-update-v0.103.0/pkg_name.tar.gz" - backupDir string // "work_dir/agh-backup" - backupExeName string // "work_dir/agh-backup/AdGuardHome[.exe]" - updateExeName string // "work_dir/agh-update-v0.103.0/AdGuardHome[.exe]" + updateDir string // "workDir/agh-update-v0.103.0" + packageName string // "workDir/agh-update-v0.103.0/pkg_name.tar.gz" + backupDir string // "workDir/agh-backup" + backupExeName string // "workDir/agh-backup/AdGuardHome[.exe]" + updateExeName string // "workDir/agh-update-v0.103.0/AdGuardHome[.exe]" unpackedFiles []string - // cached version.json to avoid hammering github.io for each page reload - versionJSON []byte - versionCheckLastTime time.Time + newVersion string + packageURL string + + // Cached fields to prevent too many API requests. + prevCheckError error + prevCheckTime time.Time + prevCheckResult VersionInfo } -// Config - updater config +// Config is the AdGuard Home updater configuration. type Config struct { Client *http.Client - VersionURL string // version.json URL - VersionString string - OS string // GOOS - Arch string // GOARCH - ARMVersion string // ARM version, e.g. "6" - NewVersion string // VersionInfo.NewVersion - PackageURL string // VersionInfo.PackageURL - ConfigName string // current config file ".../AdGuardHome.yaml" - WorkDir string // updater work dir (where backup/upd dirs will be created) + Version string + Channel string + GOARCH string + GOOS string + GOARM string + GOMIPS string + + // ConfName is the name of the current configuration file. Typically, + // "AdGuardHome.yaml". + ConfName string + // WorkDir is the working directory that is used for temporary files. + WorkDir string } -// NewUpdater - creates a new instance of the Updater -func NewUpdater(cfg Config) *Updater { +// NewUpdater creates a new Updater. +func NewUpdater(conf *Config) *Updater { + u := &url.URL{ + Scheme: "https", + Host: "static.adguard.com", + Path: path.Join("adguardhome", conf.Channel, "version.json"), + } return &Updater{ - Config: cfg, + client: conf.Client, + + version: conf.Version, + channel: conf.Channel, + goarch: conf.GOARCH, + goos: conf.GOOS, + goarm: conf.GOARM, + gomips: conf.GOMIPS, + + confName: conf.ConfName, + workDir: conf.WorkDir, + versionCheckURL: u.String(), + + mu: &sync.RWMutex{}, } } -// DoUpdate - conducts the auto-update -// 1. Downloads the update file -// 2. Unpacks it and checks the contents -// 3. Backups the current version and configuration -// 4. Replaces the old files -func (u *Updater) DoUpdate() error { +// Update performs the auto-update. +func (u *Updater) Update() error { + u.mu.Lock() + defer u.mu.Unlock() + err := u.prepare() if err != nil { return err @@ -72,7 +116,7 @@ func (u *Updater) DoUpdate() error { defer u.clean() - err = u.downloadPackageFile(u.PackageURL, u.packageName) + err = u.downloadPackageFile(u.packageURL, u.packageName) if err != nil { return err } @@ -84,7 +128,6 @@ func (u *Updater) DoUpdate() error { err = u.check() if err != nil { - u.clean() return err } @@ -101,40 +144,57 @@ func (u *Updater) DoUpdate() error { return nil } -func (u *Updater) prepare() error { - u.updateDir = filepath.Join(u.WorkDir, fmt.Sprintf("agh-update-%s", u.NewVersion)) +// NewVersion returns the available new version. +func (u *Updater) NewVersion() (nv string) { + u.mu.RLock() + defer u.mu.RUnlock() - _, pkgNameOnly := filepath.Split(u.PackageURL) - if len(pkgNameOnly) == 0 { + return u.newVersion +} + +// VersionCheckURL returns the version check URL. +func (u *Updater) VersionCheckURL() (vcu string) { + u.mu.RLock() + defer u.mu.RUnlock() + + return u.versionCheckURL +} + +func (u *Updater) prepare() error { + u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.newVersion)) + + _, pkgNameOnly := filepath.Split(u.packageURL) + if pkgNameOnly == "" { return fmt.Errorf("invalid PackageURL") } + u.packageName = filepath.Join(u.updateDir, pkgNameOnly) - u.backupDir = filepath.Join(u.WorkDir, "agh-backup") + u.backupDir = filepath.Join(u.workDir, "agh-backup") exeName := "AdGuardHome" - if u.OS == "windows" { + if u.goos == "windows" { exeName = "AdGuardHome.exe" } u.backupExeName = filepath.Join(u.backupDir, exeName) u.updateExeName = filepath.Join(u.updateDir, exeName) - log.Info("Updating from %s to %s. URL:%s", - u.VersionString, u.NewVersion, u.PackageURL) + log.Info("Updating from %s to %s. URL:%s", version.Version(), u.newVersion, u.packageURL) - // If the binary file isn't found in working directory, we won't be able to auto-update - // Getting the full path to the current binary file on UNIX and checking write permissions - // is more difficult. - u.currentExeName = filepath.Join(u.WorkDir, exeName) + // If the binary file isn't found in working directory, we won't be able + // to auto-update. Getting the full path to the current binary file on + // Unix and checking write permissions is more difficult. + u.currentExeName = filepath.Join(u.workDir, exeName) if !util.FileExists(u.currentExeName) { return fmt.Errorf("executable file %s doesn't exist", u.currentExeName) } + return nil } func (u *Updater) unpack() error { var err error - _, pkgNameOnly := filepath.Split(u.PackageURL) + _, pkgNameOnly := filepath.Split(u.packageURL) log.Debug("updater: unpacking the package") if strings.HasSuffix(pkgNameOnly, ".zip") { @@ -158,7 +218,7 @@ func (u *Updater) unpack() error { func (u *Updater) check() error { log.Debug("updater: checking configuration") - err := copyFile(u.ConfigName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) + err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) if err != nil { return fmt.Errorf("copyFile() failed: %w", err) } @@ -173,27 +233,25 @@ func (u *Updater) check() error { func (u *Updater) backup() error { log.Debug("updater: backing up the current configuration") _ = os.Mkdir(u.backupDir, 0o755) - err := copyFile(u.ConfigName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) + err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) if err != nil { return fmt.Errorf("copyFile() failed: %w", err) } - // workdir/README.md -> backup/README.md - err = copySupportingFiles(u.unpackedFiles, u.WorkDir, u.backupDir) + wd := u.workDir + err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir) if err != nil { return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", - u.WorkDir, u.backupDir, err) + wd, u.backupDir, err) } return nil } func (u *Updater) replace() error { - // update/README.md -> workdir/README.md - err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.WorkDir) + err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir) if err != nil { - return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", - u.updateDir, u.WorkDir, err) + return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", u.updateDir, u.workDir, err) } log.Debug("updater: renaming: %s -> %s", u.currentExeName, u.backupExeName) @@ -202,7 +260,7 @@ func (u *Updater) replace() error { return err } - if u.OS == "windows" { + if u.goos == "windows" { // rename fails with "File in use" error err = copyFile(u.updateExeName, u.currentExeName) } else { @@ -211,7 +269,9 @@ func (u *Updater) replace() error { if err != nil { return err } + log.Debug("updater: renamed: %s -> %s", u.updateExeName, u.currentExeName) + return nil } @@ -226,7 +286,7 @@ const MaxPackageFileSize = 32 * 1024 * 1024 // Download package file and save it to disk func (u *Updater) downloadPackageFile(url, filename string) error { - resp, err := u.Client.Get(url) + resp, err := u.client.Get(url) if err != nil { return fmt.Errorf("http request failed: %w", err) } @@ -288,7 +348,7 @@ func tarGzFileUnpack(tarfile, outdir string) ([]string, error) { } _, inputNameOnly := filepath.Split(header.Name) - if len(inputNameOnly) == 0 { + if inputNameOnly == "" { continue } @@ -355,7 +415,7 @@ func zipFileUnpack(zipfile, outdir string) ([]string, error) { fi := zf.FileInfo() inputNameOnly := fi.Name() - if len(inputNameOnly) == 0 { + if inputNameOnly == "" { continue } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 00000000..abadc17b --- /dev/null +++ b/internal/updater/updater_test.go @@ -0,0 +1,316 @@ +package updater + +import ( + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/stretchr/testify/assert" +) + +// TODO(a.garipov): Rewrite these tests. + +func TestMain(m *testing.M) { + testutil.DiscardLogOutput(m) +} + +func startHTTPServer(data string) (l net.Listener, portStr string) { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(data)) + }) + + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + + go func() { _ = http.Serve(listener, mux) }() + return listener, strconv.FormatUint(uint64(listener.Addr().(*net.TCPAddr).Port), 10) +} + +func TestUpdateGetVersion(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", + "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", + "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", + "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", + "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", + "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", + "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", + "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", + "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", + "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", + "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", + "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", + "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", + "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", + "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", + "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", + "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", + "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: "beta", + GOARCH: "arm", + GOOS: "linux", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", "beta", "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + assert.True(t, info.CanAutoUpdate) + + // check cached + _, err = u.VersionInfo(false) + assert.Nil(t, err) +} + +func TestUpdate(t *testing.T) { + // TODO(a.garipov): Uncomment and remove the code below in Go 1.15. + // + // wd := t.TempDir() + wd, err := ioutil.TempDir("", "aghtest") + assert.Nil(t, err) + t.Cleanup(func() { assert.Nil(t, os.RemoveAll(wd)) }) + + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome"), []byte("AdGuardHome"), 0o755)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "README.md"), []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "LICENSE.txt"), []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.yaml"), []byte("AdGuardHome.yaml"), 0o644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("testdata/AdGuardHome.tar.gz") + assert.Nil(t, err) + l, lport := startHTTPServer(string(pkgData)) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: "AdGuardHome.tar.gz", + } + + u.workDir = wd + u.confName = filepath.Join(u.workDir, "AdGuardHome.yaml") + u.newVersion = "v0.103.1" + u.packageURL = fakeURL.String() + + assert.Nil(t, u.prepare()) + u.currentExeName = filepath.Join(wd, "AdGuardHome") + assert.Nil(t, u.downloadPackageFile(u.packageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome", string(d)) + + // check updated files + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome")) + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "README.md")) + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "LICENSE.txt")) + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} + +func TestUpdateWindows(t *testing.T) { + // TODO(a.garipov): Uncomment and remove the code below in Go 1.15. + // + // wd := t.TempDir() + wd, err := ioutil.TempDir("", "aghtest") + assert.Nil(t, err) + t.Cleanup(func() { assert.Nil(t, os.RemoveAll(wd)) }) + + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.exe"), []byte("AdGuardHome.exe"), 0o755)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "README.md"), []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "LICENSE.txt"), []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.yaml"), []byte("AdGuardHome.yaml"), 0o644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("testdata/AdGuardHome.zip") + assert.Nil(t, err) + + l, lport := startHTTPServer(string(pkgData)) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + GOOS: "windows", + Version: "v0.103.0", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: "AdGuardHome.zip", + } + + u.workDir = wd + u.confName = filepath.Join(u.workDir, "AdGuardHome.yaml") + u.newVersion = "v0.103.1" + u.packageURL = fakeURL.String() + + assert.Nil(t, u.prepare()) + u.currentExeName = filepath.Join(wd, "AdGuardHome.exe") + assert.Nil(t, u.downloadPackageFile(u.packageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.exe")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.exe", string(d)) + + // check updated files + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.exe")) + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "README.md")) + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "LICENSE.txt")) + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} + +func TestUpdater_VersionInto_ARM(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: "beta", + GOARCH: "arm", + GOOS: "linux", + GOARM: "7", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", "beta", "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + assert.True(t, info.CanAutoUpdate) +} + +func TestUpdater_VersionInto_MIPS(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_linux_mips_softfloat": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: "beta", + GOARCH: "mips", + GOOS: "linux", + GOMIPS: "softfloat", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", "beta", "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + assert.True(t, info.CanAutoUpdate) +} diff --git a/internal/util/helpers.go b/internal/util/helpers.go index 2770fa44..b575b269 100644 --- a/internal/util/helpers.go +++ b/internal/util/helpers.go @@ -26,7 +26,7 @@ func ContainsString(strs []string, str string) bool { // FileExists returns true if file exists. func FileExists(fn string) bool { _, err := os.Stat(fn) - return err == nil + return err == nil || !os.IsNotExist(err) } // RunCommand runs shell command. diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..25fff528 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,53 @@ +// Package version contains AdGuard Home version information. +package version + +import ( + "fmt" + "runtime" +) + +// These are set by the linker. Unfortunately we cannot set constants during +// linking, and Go doesn't have a concept of immutable variables, so to be +// thorough we have to only export them through getters. +// +// TODO(a.garipov): Find out if we can get GOARM and GOMIPS values the same way +// we can GOARCH and GOOS. +var ( + channel string + goarm string + gomips string + version string +) + +// Channel returns the current AdGuard Home release channel. +func Channel() (v string) { + return channel +} + +// Full returns the full current version of AdGuard Home. +func Full() (v string) { + msg := "AdGuard Home, version %s, channel %s, arch %s %s" + if goarm != "" { + msg = msg + " v" + goarm + } else if gomips != "" { + msg = msg + " " + gomips + } + + return fmt.Sprintf(msg, version, channel, runtime.GOOS, runtime.GOARCH) +} + +// GOARM returns the GOARM value used to build the current AdGuard Home release. +func GOARM() (v string) { + return goarm +} + +// GOMIPS returns the GOMIPS value used to build the current AdGuard Home +// release. +func GOMIPS() (v string) { + return gomips +} + +// Version returns the AdGuard Home build version. +func Version() (v string) { + return version +} diff --git a/main.go b/main.go index be08b192..cf69ed04 100644 --- a/main.go +++ b/main.go @@ -6,18 +6,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/home" ) -// version is the release version. It is set by the linker. -var version = "undefined" - -// channel is the release channel. It is set by the linker. -var channel = "release" - -// goarm is the GOARM value. It is set by the linker. -var goarm = "" - -// gomips is the GOMIPS value. It is set by the linker. -var gomips = "" - func main() { - home.Main(version, channel, goarm, gomips) + home.Main() } diff --git a/scripts/make/build-release.sh b/scripts/make/build-release.sh index 4be45616..b191ac9b 100644 --- a/scripts/make/build-release.sh +++ b/scripts/make/build-release.sh @@ -319,6 +319,17 @@ echo "{ \"selfupdate_min_version\": \"0.0\", " >> "$version_json" +# Add the old object keys for compatibility with pre-v0.105.0 MIPS that +# did not mention the softfloat variant. +# +# TODO(a.garipov): Remove this around the time we hit v0.107.0. +echo " + \"download_linux_mips\": \"${version_download_url}/AdGuardHome_linux_mips_softfloat.tar.gz\", + \"download_linux_mipsle\": \"${version_download_url}/AdGuardHome_linux_mipsle_softfloat.tar.gz\", + \"download_linux_mips64\": \"${version_download_url}/AdGuardHome_linux_mips64_softfloat.tar.gz\", + \"download_linux_mips64le\": \"${version_download_url}/AdGuardHome_linux_mips64le_softfloat.tar.gz\", +" >> "$version_json" + ( # Use +f here so that ls works and we don't need to use find. set +f diff --git a/scripts/make/go-build.sh b/scripts/make/go-build.sh index a2a4a93a..f9702c35 100644 --- a/scripts/make/go-build.sh +++ b/scripts/make/go-build.sh @@ -54,17 +54,18 @@ esac # TODO(a.garipov): Additional validation? version="$VERSION" -# Set the linker flags accordingly: set the channel and the versio as -# well as goarm and gomips variable values, if the variables are set and -# are not empty. -ldflags="-s -w -X main.version=${version}" -ldflags="${ldflags} -X main.channel=${channel}" +# Set the linker flags accordingly: set the realease channel and the +# current version as well as goarm and gomips variable values, if the +# variables are set and are not empty. +readonly version_pkg='github.com/AdguardTeam/AdGuardHome/internal/version' +ldflags="-s -w -X ${version_pkg}.version=${version}" +ldflags="${ldflags} -X ${version_pkg}.channel=${channel}" if [ "${GOARM:-}" != '' ] then - ldflags="${ldflags} -X main.goarm=${GOARM}" + ldflags="${ldflags} -X ${version_pkg}.goarm=${GOARM}" elif [ "${GOMIPS:-}" != '' ] then - ldflags="${ldflags} -X main.gomips=${GOMIPS}" + ldflags="${ldflags} -X ${version_pkg}.gomips=${GOMIPS}" fi # Allow users to limit the build's parallelism.