Fixed hanging upgrades. Fixes #1199.

This commit is contained in:
Corey Butler
2024-12-30 16:22:44 -06:00
parent c85c7285c6
commit d391b309b6
3 changed files with 200 additions and 46 deletions

View File

@@ -26,6 +26,7 @@ import (
"nvm/file"
"nvm/node"
"nvm/upgrade"
"nvm/utility"
"nvm/web"
"github.com/blang/semver"
@@ -172,9 +173,18 @@ func init() {
}
}
}
// Turn on debugging output
for _, arg := range os.Args[1:] {
if strings.ToLower(strings.ReplaceAll(arg, "-", "")) == "verbose" {
utility.EnableDebugLogs()
break
}
}
}
func main() {
utility.DebugLogf("command: %v", strings.Join(os.Args, " "))
args := os.Args
detail := ""
procarch := arch.Validate(env.arch)
@@ -193,6 +203,8 @@ func main() {
return
}
utility.DebugLogf("arch: %v", procarch)
if args[1] != "version" && args[1] != "--version" && args[1] != "v" && args[1] != "-v" && args[1] != "--v" {
setup()
}
@@ -689,11 +701,28 @@ func install(version string, cpuarch string) {
return
}
}
if (cpuarch == "arm64" || cpuarch == "all") && !node.IsVersionInstalled(root, version, "arm64") {
success := web.GetNodeJS(root, version, "arm64", append64)
if !success {
status <- Status{Err: fmt.Errorf("failed to download v%v arm 64-bit executable", version)}
return
}
}
if file.Exists(filepath.Join(root, "v"+version, "node_modules", "npm")) {
utility.DebugLogf("move %v to %v", filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version))
if rnerr := os.Rename(filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version)); rnerr != nil {
status <- Status{Err: err}
}
utility.DebugFn(func() {
cmd := exec.Command("cmd", "/C", "dir", filepath.Join(env.root, "v"+version))
out, err := cmd.CombinedOutput()
if err != nil {
utility.DebugLog(err.Error())
} else {
utility.DebugLog(string(out))
}
})
if show_progress {
status <- Status{Text: "Configuring npm..."}
@@ -1577,11 +1606,16 @@ func checkLocalEnvironment() {
}
} else {
if fileInfo.Mode()&os.ModeSymlink != 0 {
targetPath, _ := os.Readlink(symlink)
targetFileInfo, _ := os.Lstat(targetPath)
if !targetFileInfo.Mode().IsDir() {
problems = append(problems, "NVM_SYMLINK is a symlink linking to a file instead of a directory.")
targetPath, err := os.Readlink(symlink)
if err != nil {
problems = append(problems, fmt.Sprintf("SYMLINK_READ Error: %v", err))
} else {
targetFileInfo, err := os.Lstat(targetPath)
if err != nil {
problems = append(problems, fmt.Sprintf("SYMLINK_READ Error: %v", err))
} else if !targetFileInfo.Mode().IsDir() {
problems = append(problems, "NVM_SYMLINK is a symlink linking to a file instead of a directory.")
}
}
} else {
problems = append(problems, "NVM_SYMLINK ("+symlink+") is not a valid symlink.")
@@ -1654,6 +1688,7 @@ func checkLocalEnvironment() {
colorize = false
}
update, checkerr := upgrade.Get()
if checkerr == nil {
if len(update.Warnings) > 0 {
fmt.Println("")
@@ -1661,7 +1696,10 @@ func checkLocalEnvironment() {
for _, warning := range update.Warnings {
upgrade.Warn(warning, colorize)
}
if len(update.Warnings) > 0 {
for _, warning := range update.VersionWarnings {
upgrade.Warn(warning, colorize)
}
if len(update.Warnings) > 0 || len(update.VersionWarnings) > 0 {
fmt.Println("")
}
}

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"nvm/author"
"nvm/semver"
"nvm/utility"
"os"
"os/exec"
"os/signal"
@@ -25,7 +26,8 @@ import (
)
const (
UPDATE_URL = "https://gist.githubusercontent.com/coreybutler/a12af0f17956a0f25b60369b5f8a661a/raw/nvm4w.json"
UPDATE_URL = "https://api.github.com/repos/coreybutler/nvm-windows/releases/latest"
ALERTS_URL = "https://author.io/nvm4w/feed/alerts"
// Color codes
yellow = "\033[33m"
reset = "\033[0m"
@@ -70,6 +72,12 @@ type Update struct {
SourceURL string `json:"sourceTpl"`
}
type Release struct {
Version string `json:"name"`
Assets []map[string]interface{} `json:"assets"`
Publish time.Time `json:"published_at"`
}
func Run(version string) error {
show_progress := false
for _, arg := range os.Args[2:] {
@@ -94,6 +102,32 @@ func Run(version string) error {
os.Exit(0)
}()
go func() {
for {
select {
case s := <-status:
if s.Warn != "" {
Warn(s.Warn)
}
if s.Err != nil {
fmt.Println(s.Err)
os.Exit(1)
}
if s.Text != "" {
fmt.Println(s.Text)
}
if s.Done {
fmt.Println("Upgrade complete")
return
}
}
}
}()
time.Sleep(300 * time.Millisecond)
return run(version, status)
}
@@ -111,6 +145,13 @@ func Run(version string) error {
for {
select {
case s := <-status:
if s.Warn != "" {
display(Notification{
Message: s.Warn,
Icon: "nvm",
})
}
if s.Cancel {
display(Notification{
Title: "Installation Canceled",
@@ -164,13 +205,6 @@ func Run(version string) error {
return
}
if s.Warn != "" {
display(Notification{
Message: s.Warn,
Icon: "nvm",
})
}
var empty string
if s.Text != empty && len(strings.TrimSpace(s.Text)) > 0 {
dlg.Text(s.Text)
@@ -236,7 +270,6 @@ func Run(version string) error {
func run(version string, status chan Status, updateMetadata ...*Update) error {
args := os.Args[2:]
colorize := true
if err := EnableVirtualTerminalProcessing(); err != nil {
colorize = false
@@ -256,7 +289,6 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
for _, warning := range update.Warnings {
status <- Status{Warn: warning}
Warn(warning, colorize)
}
verbose := false
@@ -335,29 +367,27 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
}
fmt.Println("")
}
fmt.Printf("upgrading from v%s-->%s\n", version, highlight(update.Version))
status <- Status{Text: "downloading..."}
fmt.Printf("upgrading from v%s-->%s\n\ndownloading...\n", version, highlight(update.Version))
} else {
status <- Status{Text: "nvm is up to date", Done: true}
fmt.Println("nvm is up to date")
return nil
}
// Make temp directory
tmp, err := os.MkdirTemp("", "nvm-upgrade-*")
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("error: failed to create temporary directory: %v\n", err)
status <- Status{Err: fmt.Errorf("error: failed to create temporary directory: %v\n", err)}
}
defer os.RemoveAll(tmp)
// Download the new app
source := fmt.Sprintf(update.SourceURL, update.Version)
source := update.SourceURL
// source := fmt.Sprintf(update.SourceURL, update.Version)
// source := fmt.Sprintf(update.SourceURL, "1.1.11") // testing
body, err := get(source)
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("error: failed to download new version: %v\n", err)
status <- Status{Err: fmt.Errorf("error: failed to download new version: %v\n", err)}
}
os.WriteFile(filepath.Join(tmp, "assets.zip"), body, os.ModePerm)
@@ -375,49 +405,42 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
checksumFile := filepath.Join(tmp, "assets.zip.checksum.txt") // path to the checksum file
// Step 1: Compute the MD5 checksum of the file
fmt.Println("verifying checksum...")
status <- Status{Text: "verifying checksum..."}
computedChecksum, err := computeMD5Checksum(filePath)
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("Error computing checksum: %v", err)
status <- Status{Err: fmt.Errorf("Error computing checksum: %v", err)}
}
// Step 2: Read the checksum from the .checksum.txt file
storedChecksum, err := readChecksumFromFile(checksumFile)
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("Error readirng checksum from file: %v", err)
}
// Step 3: Compare the computed checksum with the stored checksum
if strings.ToLower(computedChecksum) != strings.ToLower(storedChecksum) {
status <- Status{Err: fmt.Errorf("cannot validate update file (checksum mismatch)")}
return fmt.Errorf("cannot validate update file (checksum mismatch)")
}
fmt.Println("extracting update...")
status <- Status{Text: "extracting update..."}
if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil {
status <- Status{Err: err}
return err
}
// Get any additional assets
if len(update.Assets) > 0 {
status <- Status{Text: fmt.Sprintf("downloading %d additional assets...", len(update.Assets))}
fmt.Printf("downloading %d additional assets...\n", len(update.Assets))
for _, asset := range update.Assets {
var assetURL string
if !strings.HasPrefix(asset, "http") {
assetURL = fmt.Sprintf(update.SourceURL, asset)
assetURL = update.SourceURL
// assetURL = fmt.Sprintf(update.SourceURL, asset)
} else {
assetURL = asset
}
assetBody, err := get(assetURL)
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("error: failed to download asset: %v\n", err)
status <- Status{Err: fmt.Errorf("error: failed to download asset: %v\n", err)}
}
assetPath := filepath.Join(tmp, "assets", asset)
@@ -438,21 +461,18 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
}
// Backup current version to zip
fmt.Println("applying update...")
status <- Status{Text: "applying update..."}
currentExe, _ := os.Executable()
currentPath := filepath.Dir(currentExe)
bkp, err := os.MkdirTemp("", "nvm-backup-*")
if err != nil {
status <- Status{Err: fmt.Errorf("error: failed to create backup directory: %v\n", err)}
return fmt.Errorf("error: failed to create backup directory: %v\n", err)
}
defer os.RemoveAll(bkp)
err = zipDirectory(currentPath, filepath.Join(bkp, "backup.zip"))
if err != nil {
status <- Status{Err: fmt.Errorf("error: failed to create backup: %v\n", err)}
return fmt.Errorf("error: failed to create backup: %v\n", err)
}
os.MkdirAll(filepath.Join(currentPath, ".update"), os.ModePerm)
@@ -470,7 +490,6 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
err = nvmtestcmd.Run()
if err != nil {
status <- Status{Err: err}
fmt.Println("error running nvm.exe:", err)
}
}
@@ -486,8 +505,7 @@ func run(version string, status chan Status, updateMetadata ...*Update) error {
if fsutil.IsExecutable(filepath.Join(tmp, "assets", "update.exe")) {
err = copyFile(filepath.Join(tmp, "assets", "update.exe"), filepath.Join(currentPath, ".update", "update.exe"))
if err != nil {
status <- Status{Err: err}
fmt.Println(fmt.Errorf("error: failed to copy update.exe: %v\n", err))
status <- Status{Err: fmt.Errorf("error: failed to copy update.exe: %v\n", err)}
os.Exit(1)
}
}
@@ -665,7 +683,7 @@ exit /b 0
// Exit the current process (delay for cleanup)
time.Sleep(300 * time.Millisecond)
status <- Status{Text: "Restarting app...", Done: true}
status <- Status{Text: "restarting app...", Done: true}
time.Sleep(2 * time.Second)
os.Exit(0)
}
@@ -690,34 +708,103 @@ func get(url string, verbose ...bool) ([]byte, error) {
if len(verbose) == 0 || verbose[0] {
fmt.Printf(" GET %s\n", url)
}
resp, err := http.Get(url)
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return []byte{}, err
}
req.Header.Set("User-Agent", "nvm-windows")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Pragma", "no-cache")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []byte{}, fmt.Errorf("error: received status code %d\n", resp.StatusCode)
return []byte{}, fmt.Errorf("error: received status code %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
func checkForUpdate(url string) (*Update, error) {
u := Update{}
u := Update{Assets: []string{}, Warnings: []string{}, VersionWarnings: []string{}}
r := Release{}
// Make the HTTP GET request
utility.DebugLogf("checking for updates at %s", url)
body, err := get(url, false)
if err != nil {
return &u, fmt.Errorf("error: reading response body: %v", err)
}
// Parse JSON into the struct
err = json.Unmarshal(body, &u)
utility.DebugLogf("Received:\n%s", string(body))
err = json.Unmarshal(body, &r)
if err != nil {
return &u, fmt.Errorf("error: parsing update: %v", err)
return &u, fmt.Errorf("error: parsing release: %v", err)
}
u.Version = r.Version
utility.DebugLogf("latest version: %s", u.Version)
// Comment the next line when development is complete
// u.Version = "2.0.0"
for _, asset := range r.Assets {
if value, exists := asset["name"]; exists && value.(string) == "update.exe" {
u.Assets = append(u.Assets, value.(string))
}
if value, exists := asset["name"]; exists && value.(string) == "nvm-noinstall.zip" {
u.SourceURL = asset["browser_download_url"].(string)
}
}
utility.DebugLogf("source URL: %s", u.SourceURL)
utility.DebugLogf("assets: %v", u.Assets)
// Get alerts
utility.DebugLogf("downloading alerts from %s", ALERTS_URL)
body, err = get(ALERTS_URL, false)
if err != nil {
utility.DebugLogf("alert download error: %v", err)
return &u, err
}
utility.DebugLogf("Received:\n%s", string(body))
var alerts map[string][]interface{}
err = json.Unmarshal(body, &alerts)
if err != nil {
utility.DebugLogf("alert parsing error: %v", err)
}
if value, exists := alerts["all"]; exists {
for _, warning := range value {
warn := warning.(map[string]interface{})
if v, exists := warn["message"]; exists {
u.Warnings = append(u.Warnings, v.(string))
}
}
}
if value, exists := alerts[u.Version]; exists {
utility.DebugLogf("version warnings exist for %v\n%v", u.Version, value)
for _, warning := range value {
utility.DebugLogf("warning: %v", warning)
warn := warning.(map[string]interface{})
if v, exists := warn["message"]; exists {
u.VersionWarnings = append(u.VersionWarnings, v.(string))
}
}
}
utility.DebugLogf("warnings: %v", u.Warnings)
utility.DebugLogf("version warnings: %v", u.VersionWarnings)
return &u, nil
}

View File

@@ -11,12 +11,15 @@ import (
"nvm/arch"
"nvm/file"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"nvm/utility"
"archive/zip"
"github.com/blang/semver"
@@ -213,6 +216,7 @@ func Download(url string, target string, version string) bool {
}
func GetNodeJS(root string, v string, a string, append bool) bool {
utility.DebugLogf("running GetNodeJS with root: %v, v%v, arch: %v, append: %v", root, v, a, append)
a = arch.Validate(a)
vpre := ""
@@ -241,6 +245,8 @@ func GetNodeJS(root string, v string, a string, append bool) bool {
url := getNodeUrl(v, vpre, a, append)
utility.DebugLogf("download url: %v", url)
if url == "" {
//No url should mean this version/arch isn't available
fmt.Println("Node.js v" + v + " " + a + "bit isn't available right now.")
@@ -253,9 +259,11 @@ func GetNodeJS(root string, v string, a string, append bool) bool {
fmt.Println("Downloading node.js version " + v + " (" + a + "-bit)... ")
if Download(url, fileName, v) {
utility.DebugLog("download succeeded")
// Extract the zip file
if strings.HasSuffix(url, ".zip") {
fmt.Println("Extracting node and npm...")
utility.DebugLogf("extracting %v to %v", fileName, root+"\\v"+v)
err := unzip(fileName, root+"\\v"+v)
if err != nil {
fmt.Println("Error extracting from Node archive: " + err.Error())
@@ -264,6 +272,7 @@ func GetNodeJS(root string, v string, a string, append bool) bool {
if err != nil {
fmt.Printf("Failed to remove %v after failed extraction. Please remove manually.", fileName)
}
utility.DebugLogf("removed %v", fileName)
return false
}
@@ -272,21 +281,36 @@ func GetNodeJS(root string, v string, a string, append bool) bool {
if err != nil {
fmt.Printf("Failed to remove %v after successful extraction. Please remove manually.", fileName)
}
utility.DebugLogf("removed %v", fileName)
zip := root + "\\v" + v + "\\" + strings.Replace(filepath.Base(url), ".zip", "", 1)
utility.DebugLogf("moving %v to %v", zip, root+"\\v"+v)
err = fs.Move(zip, root+"\\v"+v, true)
if err != nil {
fmt.Println("ERROR moving file: " + err.Error())
}
utility.DebugLog("move succeeded")
err = os.RemoveAll(zip)
if err != nil {
fmt.Printf("Failed to remove %v after successful extraction. Please remove manually.", zip)
}
utility.DebugLogf("removed %v", zip)
utility.DebugFn(func() {
cmd := exec.Command("cmd", "/C", "dir", root+"\\v"+v)
out, err := cmd.CombinedOutput()
if err != nil {
utility.DebugLog(err.Error())
} else {
utility.DebugLog(string(out))
}
})
}
fmt.Println("Complete")
return true
} else {
utility.DebugLog("download failed")
return false
}
}
@@ -296,9 +320,12 @@ func GetNodeJS(root string, v string, a string, append bool) bool {
func GetNpm(root string, v string) bool {
url := GetFullNpmUrl("v" + v + ".zip")
// temp directory to download the .zip file
tempDir := root + "\\temp"
utility.DebugLogf("downloading npm from %v to %v", url, tempDir)
// if the temp directory doesn't exist, create it
if !file.Exists(tempDir) {
fmt.Println("Creating " + tempDir + "\n")
@@ -312,9 +339,11 @@ func GetNpm(root string, v string) bool {
fmt.Printf("Downloading npm version " + v + "... ")
if Download(url, fileName, v) {
utility.DebugLog("npm download succeeded")
fmt.Printf("Complete\n")
return true
} else {
utility.DebugLog("npm download failed")
return false
}
}