diff --git a/pkg/hypervisor/hypervisor.go b/pkg/hypervisor/hypervisor.go index c893315293..50342e725c 100644 --- a/pkg/hypervisor/hypervisor.go +++ b/pkg/hypervisor/hypervisor.go @@ -26,6 +26,7 @@ import ( "github.com/SkycoinProject/skywire-mainnet/internal/skyenv" "github.com/SkycoinProject/skywire-mainnet/pkg/app" "github.com/SkycoinProject/skywire-mainnet/pkg/routing" + "github.com/SkycoinProject/skywire-mainnet/pkg/util/buildinfo" "github.com/SkycoinProject/skywire-mainnet/pkg/visor" ) @@ -176,6 +177,7 @@ func (hv *Hypervisor) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.Post("/visors/{pk}/restart", hv.restart()) r.Post("/visors/{pk}/exec", hv.exec()) r.Post("/visors/{pk}/update", hv.update()) + r.Get("/visors/{pk}/update/available", hv.updateAvailable()) }) }) @@ -734,12 +736,42 @@ func (hv *Hypervisor) exec() http.HandlerFunc { func (hv *Hypervisor) update() http.HandlerFunc { return hv.withCtx(hv.visorCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { - if err := ctx.RPC.Update(); err != nil { + updated, err := ctx.RPC.Update() + if err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) return } - httputil.WriteJSON(w, r, http.StatusOK, true) + output := struct { + Updated bool `json:"updated"` + }{updated} + + httputil.WriteJSON(w, r, http.StatusOK, output) + }) +} + +func (hv *Hypervisor) updateAvailable() http.HandlerFunc { + return hv.withCtx(hv.visorCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { + version, err := ctx.RPC.UpdateAvailable() + if err != nil { + httputil.WriteJSON(w, r, http.StatusInternalServerError, err) + return + } + + output := struct { + Available bool `json:"available"` + CurrentVersion string `json:"current_version"` + AvailableVersion string `json:"available_version,omitempty"` + }{ + Available: version != nil, + CurrentVersion: buildinfo.Version(), + } + + if version != nil { + output.AvailableVersion = version.String() + } + + httputil.WriteJSON(w, r, http.StatusOK, output) }) } diff --git a/pkg/util/updater/updater.go b/pkg/util/updater/updater.go index e85a628583..22b51ca273 100644 --- a/pkg/util/updater/updater.go +++ b/pkg/util/updater/updater.go @@ -71,36 +71,31 @@ func New(log *logging.Logger, restartCtx *restart.Context, appsPath string) *Upd // Update performs an update operation. // NOTE: Update may call os.Exit. -func (u *Updater) Update() (err error) { +func (u *Updater) Update() (updated bool, err error) { if !atomic.CompareAndSwapInt32(&u.updating, 0, 1) { - return ErrAlreadyStarted + return false, ErrAlreadyStarted } defer atomic.StoreInt32(&u.updating, 0) - u.log.Infof("Looking for updates") - - lastVersion, err := lastVersion() + latestVersion, err := u.UpdateAvailable() if err != nil { - return fmt.Errorf("failed to get last Skywire version: %w", err) + return false, fmt.Errorf("failed to get last Skywire version: %w", err) } - u.log.Infof("Last Skywire version: %q", lastVersion.String()) - - if !updateAvailable(lastVersion) { - u.log.Infof("You are using the latest version of Skywire") - return nil + if latestVersion == nil { + return false, nil } - u.log.Infof("Update found, version: %q", lastVersion.String()) + u.log.Infof("Update found, version: %q", latestVersion.String()) - downloadedBinariesPath, err := u.download(lastVersion.String()) + downloadedBinariesPath, err := u.download(latestVersion.String()) if err != nil { - return err + return false, err } currentBasePath := filepath.Dir(u.restartCtx.CmdPath()) if err := u.updateBinaries(downloadedBinariesPath, currentBasePath); err != nil { - return err + return false, err } if err := u.restartCurrentProcess(); err != nil { @@ -109,7 +104,7 @@ func (u *Updater) Update() (err error) { u.restore(currentVisorPath, oldVisorPath) - return err + return false, err } u.removeFiles(downloadedBinariesPath) @@ -121,7 +116,28 @@ func (u *Updater) Update() (err error) { } }() - return nil + return true, nil +} + +// UpdateAvailable checks if an update is available. +// If it is, the method returns the last available version. +// Otherwise, it returns nil. +func (u *Updater) UpdateAvailable() (*Version, error) { + u.log.Infof("Looking for updates") + + latestVersion, err := latestVersion() + if err != nil { + return nil, err + } + + u.log.Infof("Last Skywire version: %q", latestVersion.String()) + + if !needUpdate(latestVersion) { + u.log.Infof("You are using the latest version of Skywire") + return nil, nil + } + + return latestVersion, nil } func (u *Updater) exitAfterDelay(delay time.Duration) { @@ -392,7 +408,7 @@ func archiveFilename(file, version, os, arch string) string { return file + "-" + version + "-" + os + "-" + arch + archiveFormat } -func updateAvailable(last *Version) bool { +func needUpdate(last *Version) bool { current, err := currentVersion() if err != nil { // Unknown versions should be updated. @@ -402,20 +418,20 @@ func updateAvailable(last *Version) bool { return last.Cmp(current) > 0 } -func lastVersion() (*Version, error) { - html, err := lastVersionHTML() +func latestVersion() (*Version, error) { + html, err := latestVersionHTML() if err != nil { return nil, err } - return VersionFromString(extractLastVersion(string(html))) + return VersionFromString(extractLatestVersion(string(html))) } func currentVersion() (*Version, error) { return VersionFromString(buildinfo.Version()) } -func lastVersionHTML() (data []byte, err error) { +func latestVersionHTML() (data []byte, err error) { resp, err := http.Get(releaseURL) if err != nil { return nil, err @@ -430,7 +446,7 @@ func lastVersionHTML() (data []byte, err error) { return ioutil.ReadAll(resp.Body) } -func extractLastVersion(buffer string) string { +func extractLatestVersion(buffer string) string { // First occurrence is the latest version. idx := strings.Index(buffer, urlText) if idx == -1 { diff --git a/pkg/util/updater/updater_test.go b/pkg/util/updater/updater_test.go index 33c2d12283..d895f4ac3a 100644 --- a/pkg/util/updater/updater_test.go +++ b/pkg/util/updater/updater_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_extractLastVersion(t *testing.T) { +func Test_extractLatestVersion(t *testing.T) { tests := []struct { name string buffer string @@ -32,7 +32,7 @@ func Test_extractLastVersion(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - got := extractLastVersion(tc.buffer) + got := extractLatestVersion(tc.buffer) assert.Equal(t, tc.want, got) }) } diff --git a/pkg/visor/rpc.go b/pkg/visor/rpc.go index 48f741e0a1..6e06f50e2c 100644 --- a/pkg/visor/rpc.go +++ b/pkg/visor/rpc.go @@ -19,6 +19,7 @@ import ( "github.com/SkycoinProject/skywire-mainnet/pkg/transport" "github.com/SkycoinProject/skywire-mainnet/pkg/util/buildinfo" "github.com/SkycoinProject/skywire-mainnet/pkg/util/rpcutil" + "github.com/SkycoinProject/skywire-mainnet/pkg/util/updater" ) const ( @@ -499,8 +500,26 @@ func (r *RPC) Exec(cmd *string, out *[]byte) (err error) { } // Update updates visor. -func (r *RPC) Update(_ *struct{}, _ *struct{}) (err error) { - defer rpcutil.LogCall(r.log, "Update", nil)(nil, &err) +func (r *RPC) Update(_ *struct{}, updated *bool) (err error) { + defer rpcutil.LogCall(r.log, "Update", nil)(updated, &err) - return r.visor.Update() + *updated, err = r.visor.Update() + return +} + +// UpdateAvailable checks if visor update is available. +func (r *RPC) UpdateAvailable(_ *struct{}, version *updater.Version) (err error) { + defer rpcutil.LogCall(r.log, "UpdateAvailable", nil)(version, &err) + + v, err := r.visor.UpdateAvailable() + if err != nil { + return err + } + + if v == nil { + return nil + } + + *version = *v + return nil } diff --git a/pkg/visor/rpc_client.go b/pkg/visor/rpc_client.go index fa0b01984a..49f11be974 100644 --- a/pkg/visor/rpc_client.go +++ b/pkg/visor/rpc_client.go @@ -20,6 +20,7 @@ import ( "github.com/SkycoinProject/skywire-mainnet/pkg/snet/snettest" "github.com/SkycoinProject/skywire-mainnet/pkg/transport" "github.com/SkycoinProject/skywire-mainnet/pkg/util/buildinfo" + "github.com/SkycoinProject/skywire-mainnet/pkg/util/updater" ) var ( @@ -61,7 +62,8 @@ type RPCClient interface { Restart() error Exec(command string) ([]byte, error) - Update() error + Update() (bool, error) + UpdateAvailable() (*updater.Version, error) } // RPCClient provides methods to call an RPC Server. @@ -250,9 +252,25 @@ func (rc *rpcClient) Exec(command string) ([]byte, error) { } // Update calls Update. -func (rc *rpcClient) Update() error { - err := rc.Call("Update", &struct{}{}, &struct{}{}) - return err +func (rc *rpcClient) Update() (bool, error) { + var updated bool + err := rc.Call("Update", &struct{}{}, &updated) + return updated, err +} + +// UpdateAvailable calls UpdateAvailable. +func (rc *rpcClient) UpdateAvailable() (*updater.Version, error) { + var version, empty updater.Version + err := rc.Call("UpdateAvailable", &struct{}{}, &version) + if err != nil { + return nil, err + } + + if version == empty { + return nil, nil + } + + return &version, err } // MockRPCClient mocks RPCClient. @@ -614,7 +632,12 @@ func (mc *mockRPCClient) Exec(string) ([]byte, error) { return []byte("mock"), nil } -// Exec implements RPCClient. -func (mc *mockRPCClient) Update() error { - return nil +// Update implements RPCClient. +func (mc *mockRPCClient) Update() (bool, error) { + return false, nil +} + +// UpdateAvailable implements RPCClient. +func (mc *mockRPCClient) UpdateAvailable() (*updater.Version, error) { + return nil, nil } diff --git a/pkg/visor/visor.go b/pkg/visor/visor.go index 7361fc4066..efda3e5ea4 100644 --- a/pkg/visor/visor.go +++ b/pkg/visor/visor.go @@ -675,15 +675,28 @@ func (visor *Visor) Exec(command string) ([]byte, error) { return cmd.CombinedOutput() } -// Update checks if visor update is available. +// Update updates visor. +// It checks if visor update is available. // If it is, the method downloads a new visor versions, starts it and kills the current process. -func (visor *Visor) Update() error { - if err := visor.updater.Update(); err != nil { +func (visor *Visor) Update() (bool, error) { + updated, err := visor.updater.Update() + if err != nil { visor.logger.Errorf("Failed to update visor: %v", err) - return err + return false, err } - return nil + return updated, nil +} + +// UpdateAvailable checks if visor update is available. +func (visor *Visor) UpdateAvailable() (*updater.Version, error) { + version, err := visor.updater.UpdateAvailable() + if err != nil { + visor.logger.Errorf("Failed to check if visor update is available: %v", err) + return nil, err + } + + return version, nil } func (visor *Visor) setAutoStart(appName string, autoStart bool) error {