From 644dca8da202f61a4be708152f06a92c8a46c10a Mon Sep 17 00:00:00 2001 From: Tony Worm Date: Wed, 13 May 2020 21:18:12 -0600 Subject: [PATCH] add (back) update command --- .hof/Cli/cmd/mvs/cmd/update.go | 336 +++++++++++++++++++++++++++++++++ cli.cue | 2 + cmd/mvs/cmd/update.go | 336 +++++++++++++++++++++++++++++++++ cue.mods | 2 +- cue.sums | 2 + 5 files changed, 677 insertions(+), 1 deletion(-) create mode 100644 .hof/Cli/cmd/mvs/cmd/update.go create mode 100644 cmd/mvs/cmd/update.go diff --git a/.hof/Cli/cmd/mvs/cmd/update.go b/.hof/Cli/cmd/mvs/cmd/update.go new file mode 100644 index 0000000..8925e46 --- /dev/null +++ b/.hof/Cli/cmd/mvs/cmd/update.go @@ -0,0 +1,336 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/parnurzeal/gorequest" + "github.com/spf13/cobra" + + "github.com/hofstadter-io/mvs/cmd/mvs/ga" + "github.com/hofstadter-io/mvs/cmd/mvs/verinfo" +) + +var UpdateLong = `Print the build version for mvs` + +var ( + UpdateCheckFlag bool + + UpdateStarted bool + UpdateErrored bool + UpdateChecked bool + UpdateAvailable *ProgramVersion + UpdateData []interface{} +) + +func init() { + UpdateCmd.Flags().BoolVarP(&UpdateCheckFlag, "check", "", false, "set to only check for an update") +} + +const updateMessage = ` +Updates available. v%s -> %s (latest) + + run 'mvs update' to get the latest. + +` + +// TODO, add a curl to the above? or os specific? + +var UpdateCmd = &cobra.Command{ + + Use: "update", + + Short: "update the dma tool", + + Long: UpdateLong, + + PreRun: func(cmd *cobra.Command, args []string) { + ga.SendGaEvent("update", "", 0) + }, + + Run: func(cmd *cobra.Command, args []string) { + + latest, err := CheckUpdate(true) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + // Semver Check? + cur := ProgramVersion{Version: "v" + verinfo.Version} + if latest.Version == cur.Version || cur.Version == "vLocal" { + return + } else { + if UpdateCheckFlag { + return + } + } + + err = InstallLatest() + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + }, +} + +func init() { + RootCmd.AddCommand(UpdateCmd) + + go CheckUpdate(false) +} + +type ProgramVersion struct { + Version string + URL string +} + +func CheckUpdate(manual bool) (ver ProgramVersion, err error) { + if !manual && os.Getenv("MVS_UPDATES_DISABLED") != "" { + return + } + UpdateStarted = true + cur := ProgramVersion{Version: "v" + verinfo.Version} + + checkURL := "https://api.github.com/repos/hofstadter-io/mvs/releases/latest" + + req := gorequest.New().Get(checkURL). + Query("current=" + cur.Version). + Query("manual=" + fmt.Sprint(manual)) + resp, b, errs := req.EndBytes() + UpdateErrored = true + + check := "http2: server sent GOAWAY and closed the connection" + if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { + // fmt.Println("errs:", errs) + return ver, errs[0] + } + + if len(errs) != 0 || resp.StatusCode >= 500 { + return ver, fmt.Errorf("Internal Error: " + string(b)) + } + if resp.StatusCode >= 400 { + if resp.StatusCode == 404 { + return ver, fmt.Errorf("No releases available :[") + } + return ver, fmt.Errorf("Bad Request: " + string(b)) + } + + UpdateErrored = false + // fmt.Println(string(b)) + + var gh map[string]interface{} + err = json.Unmarshal(b, &gh) + if err != nil { + return ver, err + } + + nameI, ok := gh["name"] + if !ok { + return ver, fmt.Errorf("Internal Error: could not find version in update check response") + } + name, ok := nameI.(string) + if !ok { + return ver, fmt.Errorf("Internal Error: version is not a string in update check response") + } + ver.Version = name + + if !manual { + UpdateChecked = true + + // Semver Check? + if ver.Version != cur.Version && cur.Version != "vLocal" { + UpdateAvailable = &ver + } + + return ver, nil + } + + // This goes here and signals else where that we got the request back + UpdateChecked = true + + // Semver Check? + if ver.Version != cur.Version && cur.Version != "vLocal" { + UpdateAvailable = &ver + aI, ok := gh["assets"] + if ok { + a, aok := aI.([]interface{}) + if aok { + UpdateData = a + } + } + } + + return ver, nil +} + +func WaitPrintUpdateAvail() { + for i := 0; i < 20 && !UpdateStarted && !UpdateChecked && !UpdateErrored; i++ { + time.Sleep(50 * time.Millisecond) + } + PrintUpdateAvailable() +} + +func PrintUpdateAvailable() { + if UpdateChecked && UpdateAvailable != nil { + fmt.Printf(updateMessage, verinfo.Version, UpdateAvailable.Version) + } +} + +func InstallLatest() (err error) { + fmt.Printf("Installing mvs@%s\n", UpdateAvailable.Version) + + if UpdateData == nil { + return fmt.Errorf("No update available") + } + /* + vers, err := json.MarshalIndent(UpdateData, "", " ") + if err == nil { + fmt.Println(string(vers)) + } + */ + + fmt.Println("OS/Arch", verinfo.BuildOS, verinfo.BuildArch) + + url := "" + for _, Asset := range UpdateData { + asset := Asset.(map[string]interface{}) + U := asset["browser_download_url"].(string) + u := strings.ToLower(U) + + osOk, archOk := false, false + + switch verinfo.BuildOS { + case "linux": + if strings.Contains(u, "linux") { + osOk = true + } + + case "darwin": + if strings.Contains(u, "darwin") { + osOk = true + } + + case "windows": + if strings.Contains(u, "windows") { + osOk = true + } + } + + switch verinfo.BuildArch { + case "amd64": + if strings.Contains(u, "x86_64") { + archOk = true + } + case "arm64": + if strings.Contains(u, "arm64") { + archOk = true + } + case "arm": + if strings.Contains(u, "arm") && !strings.Contains(u, "arm64") { + archOk = true + } + } + + if osOk && archOk { + url = u + break + } + } + + fmt.Println("Download URL: ", url, "\n") + + switch verinfo.BuildOS { + case "linux": + fallthrough + case "darwin": + + return downloadAndInstall(url) + + case "windows": + fmt.Println("Please downlaod and install manually from the link above.\n") + return nil + } + + return nil +} + +func downloadAndInstall(url string) error { + req := gorequest.New().Get(url) + resp, content, errs := req.EndBytes() + + check := "http2: server sent GOAWAY and closed the connection" + if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { + fmt.Println("errs:", errs) + fmt.Println("resp:", resp) + return errs[0] + } + + if len(errs) != 0 || resp.StatusCode >= 400 { + return fmt.Errorf("Error %v - %s", resp.StatusCode, string(content)) + } + + tmpfile, err := ioutil.TempFile("", "example") + if err != nil { + return err + } + + // defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + return err + } + if err := tmpfile.Close(); err != nil { + return err + } + + ex, err := os.Executable() + if err != nil { + return err + } + + real, err := filepath.EvalSymlinks(ex) + if err != nil { + return err + } + + // Sudo copy the file + cmd := exec.Command("/bin/sh", "-c", + fmt.Sprintf("export OWNER=$(ls -l %s | awk '{ print $3 \":\" $4 }') && sudo mv %s %s.backup && sudo cp %s %s && sudo chown $OWNER %s && sudo chmod 0755 %s && sudo rm %s.backup", + real, // get owner + real, real, // mv + tmpfile.Name(), real, // cp + real, // chown + real, // chmod + real, // rm + ), + ) + + // prep stdin for password + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + go func() { + defer stdin.Close() + io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") + }() + + stdoutStderr, err := cmd.CombinedOutput() + fmt.Printf("%s\n", stdoutStderr) + if err != nil { + return err + } + + UpdateAvailable = nil + UpdateData = nil + return nil +} diff --git a/cli.cue b/cli.cue index 4fa72d0..6f5de5e 100644 --- a/cli.cue +++ b/cli.cue @@ -57,6 +57,8 @@ HofGenCli: gen.#HofGenerator & { } } + Updates: true + Telemetry: "UA-103579574-5" TelemetryIdDir: "hof" diff --git a/cmd/mvs/cmd/update.go b/cmd/mvs/cmd/update.go new file mode 100644 index 0000000..8925e46 --- /dev/null +++ b/cmd/mvs/cmd/update.go @@ -0,0 +1,336 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/parnurzeal/gorequest" + "github.com/spf13/cobra" + + "github.com/hofstadter-io/mvs/cmd/mvs/ga" + "github.com/hofstadter-io/mvs/cmd/mvs/verinfo" +) + +var UpdateLong = `Print the build version for mvs` + +var ( + UpdateCheckFlag bool + + UpdateStarted bool + UpdateErrored bool + UpdateChecked bool + UpdateAvailable *ProgramVersion + UpdateData []interface{} +) + +func init() { + UpdateCmd.Flags().BoolVarP(&UpdateCheckFlag, "check", "", false, "set to only check for an update") +} + +const updateMessage = ` +Updates available. v%s -> %s (latest) + + run 'mvs update' to get the latest. + +` + +// TODO, add a curl to the above? or os specific? + +var UpdateCmd = &cobra.Command{ + + Use: "update", + + Short: "update the dma tool", + + Long: UpdateLong, + + PreRun: func(cmd *cobra.Command, args []string) { + ga.SendGaEvent("update", "", 0) + }, + + Run: func(cmd *cobra.Command, args []string) { + + latest, err := CheckUpdate(true) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + // Semver Check? + cur := ProgramVersion{Version: "v" + verinfo.Version} + if latest.Version == cur.Version || cur.Version == "vLocal" { + return + } else { + if UpdateCheckFlag { + return + } + } + + err = InstallLatest() + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + }, +} + +func init() { + RootCmd.AddCommand(UpdateCmd) + + go CheckUpdate(false) +} + +type ProgramVersion struct { + Version string + URL string +} + +func CheckUpdate(manual bool) (ver ProgramVersion, err error) { + if !manual && os.Getenv("MVS_UPDATES_DISABLED") != "" { + return + } + UpdateStarted = true + cur := ProgramVersion{Version: "v" + verinfo.Version} + + checkURL := "https://api.github.com/repos/hofstadter-io/mvs/releases/latest" + + req := gorequest.New().Get(checkURL). + Query("current=" + cur.Version). + Query("manual=" + fmt.Sprint(manual)) + resp, b, errs := req.EndBytes() + UpdateErrored = true + + check := "http2: server sent GOAWAY and closed the connection" + if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { + // fmt.Println("errs:", errs) + return ver, errs[0] + } + + if len(errs) != 0 || resp.StatusCode >= 500 { + return ver, fmt.Errorf("Internal Error: " + string(b)) + } + if resp.StatusCode >= 400 { + if resp.StatusCode == 404 { + return ver, fmt.Errorf("No releases available :[") + } + return ver, fmt.Errorf("Bad Request: " + string(b)) + } + + UpdateErrored = false + // fmt.Println(string(b)) + + var gh map[string]interface{} + err = json.Unmarshal(b, &gh) + if err != nil { + return ver, err + } + + nameI, ok := gh["name"] + if !ok { + return ver, fmt.Errorf("Internal Error: could not find version in update check response") + } + name, ok := nameI.(string) + if !ok { + return ver, fmt.Errorf("Internal Error: version is not a string in update check response") + } + ver.Version = name + + if !manual { + UpdateChecked = true + + // Semver Check? + if ver.Version != cur.Version && cur.Version != "vLocal" { + UpdateAvailable = &ver + } + + return ver, nil + } + + // This goes here and signals else where that we got the request back + UpdateChecked = true + + // Semver Check? + if ver.Version != cur.Version && cur.Version != "vLocal" { + UpdateAvailable = &ver + aI, ok := gh["assets"] + if ok { + a, aok := aI.([]interface{}) + if aok { + UpdateData = a + } + } + } + + return ver, nil +} + +func WaitPrintUpdateAvail() { + for i := 0; i < 20 && !UpdateStarted && !UpdateChecked && !UpdateErrored; i++ { + time.Sleep(50 * time.Millisecond) + } + PrintUpdateAvailable() +} + +func PrintUpdateAvailable() { + if UpdateChecked && UpdateAvailable != nil { + fmt.Printf(updateMessage, verinfo.Version, UpdateAvailable.Version) + } +} + +func InstallLatest() (err error) { + fmt.Printf("Installing mvs@%s\n", UpdateAvailable.Version) + + if UpdateData == nil { + return fmt.Errorf("No update available") + } + /* + vers, err := json.MarshalIndent(UpdateData, "", " ") + if err == nil { + fmt.Println(string(vers)) + } + */ + + fmt.Println("OS/Arch", verinfo.BuildOS, verinfo.BuildArch) + + url := "" + for _, Asset := range UpdateData { + asset := Asset.(map[string]interface{}) + U := asset["browser_download_url"].(string) + u := strings.ToLower(U) + + osOk, archOk := false, false + + switch verinfo.BuildOS { + case "linux": + if strings.Contains(u, "linux") { + osOk = true + } + + case "darwin": + if strings.Contains(u, "darwin") { + osOk = true + } + + case "windows": + if strings.Contains(u, "windows") { + osOk = true + } + } + + switch verinfo.BuildArch { + case "amd64": + if strings.Contains(u, "x86_64") { + archOk = true + } + case "arm64": + if strings.Contains(u, "arm64") { + archOk = true + } + case "arm": + if strings.Contains(u, "arm") && !strings.Contains(u, "arm64") { + archOk = true + } + } + + if osOk && archOk { + url = u + break + } + } + + fmt.Println("Download URL: ", url, "\n") + + switch verinfo.BuildOS { + case "linux": + fallthrough + case "darwin": + + return downloadAndInstall(url) + + case "windows": + fmt.Println("Please downlaod and install manually from the link above.\n") + return nil + } + + return nil +} + +func downloadAndInstall(url string) error { + req := gorequest.New().Get(url) + resp, content, errs := req.EndBytes() + + check := "http2: server sent GOAWAY and closed the connection" + if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { + fmt.Println("errs:", errs) + fmt.Println("resp:", resp) + return errs[0] + } + + if len(errs) != 0 || resp.StatusCode >= 400 { + return fmt.Errorf("Error %v - %s", resp.StatusCode, string(content)) + } + + tmpfile, err := ioutil.TempFile("", "example") + if err != nil { + return err + } + + // defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + return err + } + if err := tmpfile.Close(); err != nil { + return err + } + + ex, err := os.Executable() + if err != nil { + return err + } + + real, err := filepath.EvalSymlinks(ex) + if err != nil { + return err + } + + // Sudo copy the file + cmd := exec.Command("/bin/sh", "-c", + fmt.Sprintf("export OWNER=$(ls -l %s | awk '{ print $3 \":\" $4 }') && sudo mv %s %s.backup && sudo cp %s %s && sudo chown $OWNER %s && sudo chmod 0755 %s && sudo rm %s.backup", + real, // get owner + real, real, // mv + tmpfile.Name(), real, // cp + real, // chown + real, // chmod + real, // rm + ), + ) + + // prep stdin for password + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + go func() { + defer stdin.Close() + io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") + }() + + stdoutStderr, err := cmd.CombinedOutput() + fmt.Printf("%s\n", stdoutStderr) + if err != nil { + return err + } + + UpdateAvailable = nil + UpdateData = nil + return nil +} diff --git a/cue.mods b/cue.mods index 54adb1a..8b4a986 100644 --- a/cue.mods +++ b/cue.mods @@ -3,5 +3,5 @@ module github.com/hofstadter-io/mvs cue master require ( - github.com/hofstadter-io/hofmod-cli v0.5.3 + github.com/hofstadter-io/hofmod-cli v0.5.4 ) diff --git a/cue.sums b/cue.sums index c13a7dc..7b570d0 100644 --- a/cue.sums +++ b/cue.sums @@ -30,3 +30,5 @@ github.com/hofstadter-io/hofmod-cli v0.5.2 h1:LAtrhhLOnuPaAS/P+5E+9Rgq0XmcpRBwuS github.com/hofstadter-io/hofmod-cli v0.5.2/cue.mods h1:8TVLRMLOvfcVRZu6NqDr4VFmvuw/9Ca7YpCMo1fTRmU= github.com/hofstadter-io/hofmod-cli v0.5.3 h1:9QcITvHncO6lWHezRnM1YXkVTula6sfsHEQXQh2LI+E= github.com/hofstadter-io/hofmod-cli v0.5.3/cue.mods h1:8TVLRMLOvfcVRZu6NqDr4VFmvuw/9Ca7YpCMo1fTRmU= +github.com/hofstadter-io/hofmod-cli v0.5.4 h1:bO72JKO70dnmRBOV/c9IbTXQZuhsParhuwEhtL7+EdU= +github.com/hofstadter-io/hofmod-cli v0.5.4/cue.mods h1:8TVLRMLOvfcVRZu6NqDr4VFmvuw/9Ca7YpCMo1fTRmU=