From 90d42dfd0270f3ae890499025a10f81827ac3c8b Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:00:27 +0100 Subject: [PATCH] # This is a combination of 46 commits. # This is the 1st commit message: feat: add prompt-ui # This is the commit message #2: feat: configure and set algod data directory # This is the commit message #3: fix: RX/TX display # This is the commit message #4: fix: bit rate display for GB # This is the commit message #5: fix: configuration override order # This is the commit message #6: feat: handle invalid configuration and token gracefully # This is the commit message #7: test: fix test state # This is the commit message #8: fix: loading of custom endpoint address # This is the commit message #9: fix: loading default port # This is the commit message #10: test: clear viper settings # This is the commit message #11: fix: finds path to directory and gives cmd instruction # This is the commit message #12: feat: adds node start and node stop commands # This is the commit message #13: fix: add -y # This is the commit message #14: fix: turn script into indivudal commands # This is the commit message #15: feat: check if sudo, clarify shell # This is the commit message #16: chore: make more go idiomatic # This is the commit message #17: fix: fix proper path check # This is the commit message #18: fix: interact with systemctl, cleanup prompts # This is the commit message #19: fix: remove sudo # This is the commit message #20: fix: separate commands # This is the commit message #21: fix: proper algorand service name # This is the commit message #22: fix: calling with sudo # This is the commit message #23: chore: testing systemctl # This is the commit message #24: fix: checks algorand system service has been enabled directly # This is the commit message #25: feat: implements editAlgorandServiceFile # This is the commit message #26: fix: else statement # This is the commit message #27: fix: quick check branch # This is the commit message #28: fix: string template # This is the commit message #29: feat: adds upgrade # This is the commit message #30: chore: removeu nnecessary code # This is the commit message #31: fix: check that installed and candidate are the same # This is the commit message #32: chore: improve print # This is the commit message #33: chore: add more output # This is the commit message #34: fix: single quote # This is the commit message #35: fix: -y # This is the commit message #36: fix: systemctl # This is the commit message #37: fix: upgrade and sudo text # This is the commit message #38: chore: go mod tidy # This is the commit message #39: fix: upgrade # This is the commit message #40: feat: disable ui elements while syncing # This is the commit message #41: feat: skip account loading on syncing feat: remove offline account expires date # This is the commit message #42: feat: installs algod and sets up service on mac # This is the commit message #43: feat: refactor, + mac # This is the commit message #44: feat: adds uninstall, mac only # This is the commit message #45: fix: remove plist file # This is the commit message #46: chore: rename --- .../workflows/{test.yaml => code_test.yaml} | 2 +- cmd/node.go | 609 ------------------ cmd/node/configure.go | 330 ++++++++++ cmd/node/install.go | 344 ++++++++++ cmd/node/main.go | 21 + cmd/node/start.go | 118 ++++ cmd/node/stop.go | 111 ++++ cmd/node/uninstall.go | 99 +++ cmd/node/upgrade.go | 175 +++++ cmd/{ => node}/utils.go | 145 ++--- cmd/root.go | 20 +- cmd/root_test.go | 3 +- cmd/status.go | 5 +- internal/accounts.go | 25 +- internal/state.go | 5 + ui/pages/accounts/controller.go | 2 +- ui/pages/accounts/model.go | 26 +- ui/status.go | 25 +- ui/style/style.go | 12 +- ui/viewport.go | 31 +- 20 files changed, 1367 insertions(+), 741 deletions(-) rename .github/workflows/{test.yaml => code_test.yaml} (98%) delete mode 100644 cmd/node.go create mode 100644 cmd/node/configure.go create mode 100644 cmd/node/install.go create mode 100644 cmd/node/main.go create mode 100644 cmd/node/start.go create mode 100644 cmd/node/stop.go create mode 100644 cmd/node/uninstall.go create mode 100644 cmd/node/upgrade.go rename cmd/{ => node}/utils.go (75%) diff --git a/.github/workflows/test.yaml b/.github/workflows/code_test.yaml similarity index 98% rename from .github/workflows/test.yaml rename to .github/workflows/code_test.yaml index 2e8bb1a4..2ab07538 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/code_test.yaml @@ -1,4 +1,4 @@ -name: Tests +name: Code Tests on: pull_request: diff --git a/cmd/node.go b/cmd/node.go deleted file mode 100644 index bb3bd6e5..00000000 --- a/cmd/node.go +++ /dev/null @@ -1,609 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/exec" - "runtime" - "strings" - "syscall" - "time" - - "github.com/algorandfoundation/hack-tui/ui/style" - "github.com/spf13/cobra" -) - -var nodeCmd = &cobra.Command{ - Use: "node", - Short: "Algod installation", - Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"), -} - -var installCmd = &cobra.Command{ - Use: "install", - Short: "Install Algod", - Long: "Install Algod on your system", - Run: func(cmd *cobra.Command, args []string) { - installNode() - }, -} - -var configureCmd = &cobra.Command{ - Use: "configure", - Short: "Configure Algod", - Long: "Configure Algod settings", - Run: func(cmd *cobra.Command, args []string) { - configureNode() - }, -} - -var startCmd = &cobra.Command{ - Use: "start", - Short: "Start Algod", - Long: "Start Algod on your system (the one on your PATH).", - Run: func(cmd *cobra.Command, args []string) { - startNode() - }, -} - -var stopCmd = &cobra.Command{ - Use: "stop", - Short: "Stop Algod", - Long: "Stop the Algod process on your system.", - Run: func(cmd *cobra.Command, args []string) { - stopNode() - }, -} - -var upgradeCmd = &cobra.Command{ - Use: "upgrade", - Short: "Upgrade Algod", - Long: "Upgrade Algod (if installed with package manager).", - Run: func(cmd *cobra.Command, args []string) { - upgradeAlgod() - }, -} - -func init() { - rootCmd.AddCommand(nodeCmd) - nodeCmd.AddCommand(installCmd) - nodeCmd.AddCommand(configureCmd) - nodeCmd.AddCommand(startCmd) - nodeCmd.AddCommand(stopCmd) - nodeCmd.AddCommand(upgradeCmd) -} - -func installNode() { - fmt.Println("Checking if Algod is installed...") - - // Check if Algod is installed - if !isAlgodInstalled() { - fmt.Println("Algod is not installed. Installing...") - - // Install Algod based on OS - switch runtime.GOOS { - case "linux": - installNodeLinux() - case "darwin": - installNodeMac() - default: - panic("Unsupported OS: " + runtime.GOOS) - } - } else { - fmt.Println("Algod is already installed.") - printAlgodInfo() - } - -} - -func installNodeLinux() { - fmt.Println("Installing Algod on Linux") - - // Check that we are calling with sudo - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) - } - - var installCmds [][]string - var postInstallHint string - - // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager - - if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager - fmt.Println("Using apt package manager") - installCmds = [][]string{ - {"apt", "update"}, - {"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"}, - {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"}, - {"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`}, - {"apt", "update"}, - {"apt", "install", "-y", "algorand-devtools"}, - } - } else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get - fmt.Println("Using apt-get package manager") - installCmds = [][]string{ - {"apt-get", "update"}, - {"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"}, - {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"}, - {"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`}, - {"apt-get", "update"}, - {"apt-get", "install", "-y", "algorand-devtools"}, - } - } else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager - fmt.Println("Using dnf package manager") - installCmds = [][]string{ - {"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"}, - {"rpmkeys", "--import", "rpm_algorand.pub"}, - {"dnf", "install", "-y", "dnf-command(config-manager)"}, - {"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"}, - {"dnf", "install", "-y", "algorand-devtools"}, - {"systemctl", "start", "algorand"}, - } - } else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager - fmt.Println("Using yum package manager") - installCmds = [][]string{ - {"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"}, - {"rpmkeys", "--import", "rpm_algorand.pub"}, - {"yum", "install", "yum-utils"}, - {"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"}, - {"yum", "install", "-y", "algorand-devtools"}, - {"systemctl", "start", "algorand"}, - } - } else { - fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.") - installCmds = [][]string{ - {"mkdir", "~/node"}, - {"sh", "-c", "cd ~/node"}, - {"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"}, - {"chmod", "744", "update.sh"}, - {"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"}, - } - - postInstallHint = `You may need to add the Algorand binaries to your PATH: - export ALGORAND_DATA="$HOME/node/data" - export PATH="$HOME/node:$PATH" - ` - } - - // Run each installation command - for _, cmdArgs := range installCmds { - fmt.Println("Running command:", strings.Join(cmdArgs, " ")) - cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err) - cobra.CheckErr(err) - } - } - - if postInstallHint != "" { - fmt.Println(postInstallHint) - } -} - -func installNodeMac() { - fmt.Println("Installing Algod on macOS...") - - // Based off of the macOS installation instructions - // https://developer.algorand.org/docs/run-a-node/setup/install/#installing-on-mac - - installCmd := `mkdir ~/node - cd ~/node - wget https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh - chmod 744 update.sh - ./update.sh -i -c stable -p ~/node -d ~/node/data -n` - - postInstallHint := `You may need to add the Algorand binaries to your PATH: - export ALGORAND_DATA="$HOME/node/data" - export PATH="$HOME/node:$PATH" - ` - - // Run the installation command - err := exec.Command(installCmd).Run() - cobra.CheckErr(err) - - if postInstallHint != "" { - fmt.Println(postInstallHint) - } -} - -// TODO: configure not just data directory but algod path -func configureNode() { - if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { - panic("Unsupported OS: " + runtime.GOOS) - } - - var systemctlConfigure bool - - // Check systemctl first - if checkSystemctlAlgorandServiceCreated() { - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) - } - - if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") { - if checkSystemctlAlgorandServiceActive() { - fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.") - os.Exit(1) - } - // Edit the service file with the user's new data directory - systemctlConfigure = true - } else { - fmt.Println("Exiting...") - os.Exit(0) - } - } - - // At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file - // i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf - // ExecStart and Description will be changed to reflect the new data directory - // - - if !systemctlConfigure { - fmt.Println("Configuring Data directory for algod started through Algorun...") - } - - algorandData := os.Getenv("ALGORAND_DATA") - - // Check if ALGORAND_DATA environment variable is set - if algorandData != "" { - fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData) - fmt.Println("Inspecting the set data directory...") - - if validateAlgorandDataDir(algorandData) { - fmt.Println("Found valid Algorand Data Directory: " + algorandData) - - if systemctlConfigure { - if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") { - editAlgorandServiceFile(algorandData) - os.Exit(0) - } - } - - if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") { - fmt.Println("User chose not to set a completely new data directory.") - os.Exit(0) - } - - if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") { - newPath := promptWrapperInput("Enter the new data directory path") - - if !validateAlgorandDataDir(newPath) { - fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.") - os.Exit(1) - } - - if systemctlConfigure { - // Edit the service file - editAlgorandServiceFile(newPath) - } else { - // Affect the ALGORAND_DATA environment variable - affectALGORAND_DATA(newPath) - } - os.Exit(0) - } - } else { - fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.") - } - } else { - fmt.Println("ALGORAND_DATA environment variable not set.") - } - - // Do quick "lazy" check for existing Algorand Data directories - paths := lazyCheckAlgorandDataDirs() - - if len(paths) != 0 { - - fmt.Println("Quick check found the following potential data directories:") - for _, path := range paths { - fmt.Println("✔ " + path) - } - - if len(paths) == 1 { - if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") { - if systemctlConfigure { - // Edit the service file - editAlgorandServiceFile(paths[0]) - } else { - affectALGORAND_DATA(paths[0]) - } - os.Exit(0) - } - - } else { - - if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") { - - selectedPath := promptWrapperSelection("Select an Algorand data directory", paths) - - if systemctlConfigure { - // Edit the service file - editAlgorandServiceFile(selectedPath) - } else { - affectALGORAND_DATA(selectedPath) - } - os.Exit(0) - } - } - } - - // Deep search - if promptWrapperNo("Do you want Algorun to do a deep search for pre-existing Algorand Data directories? (y/n)") { - fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...") - os.Exit(0) - } - - fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...") - paths = deepSearchAlgorandDataDirs() - - if len(paths) == 0 { - fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.") - os.Exit(1) - } - - fmt.Println("Found Algorand data directories:") - for _, path := range paths { - fmt.Println(path) - } - - // Prompt user to select a directory - selectedPath := promptWrapperSelection("Select an Algorand data directory", paths) - - if systemctlConfigure { - editAlgorandServiceFile(selectedPath) - } else { - affectALGORAND_DATA(selectedPath) - } - os.Exit(0) -} - -// Start Algod on your system (the one on your PATH). -func startNode() { - fmt.Println("Attempting to start Algod...") - - if !isAlgodInstalled() { - fmt.Println("Algod is not installed. Please run the node install command.") - os.Exit(1) - } - - // Check if Algod is already running - if isAlgodRunning() { - fmt.Println("Algod is already running.") - os.Exit(0) - } - - startAlgodProcess() -} - -func isAlgodRunning() bool { - // Check if Algod is already running - // This works for systemctl started algorand.service as well as directly started algod - err := exec.Command("pgrep", "algod").Run() - return err == nil -} - -func startAlgodProcess() { - // Check if algod is available as a systemctl service - if checkSystemctlAlgorandServiceCreated() { - // Algod is available as a systemd service, start it using systemctl - - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) - } - - fmt.Println("Starting algod using systemctl...") - cmd := exec.Command("systemctl", "start", "algorand") - err := cmd.Run() - if err != nil { - fmt.Printf("Failed to start algod service: %v\n", err) - os.Exit(1) - } - } else { - // Algod is not available as a systemd service, start it directly - fmt.Println("Starting algod directly...") - - // Check if ALGORAND_DATA environment variable is set - fmt.Println("Checking if ALGORAND_DATA env var is set...") - algorandData := os.Getenv("ALGORAND_DATA") - - if !validateAlgorandDataDir(algorandData) { - fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.") - os.Exit(1) - } - - fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData) - - cmd := exec.Command("algod") - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - err := cmd.Start() - if err != nil { - fmt.Printf("Failed to start algod: %v\n", err) - os.Exit(1) - } - } - - // Wait for the process to start - time.Sleep(5 * time.Second) - - if isAlgodRunning() { - fmt.Println("Algod is running.") - } else { - fmt.Println("Algod failed to start.") - } -} - -// Stop the Algod process on your system. -func stopNode() { - fmt.Println("Attempting to stop Algod...") - - if !isAlgodRunning() { - fmt.Println("Algod was not running.") - os.Exit(0) - } - - stopAlgodProcess() - - time.Sleep(5 * time.Second) - - if !isAlgodRunning() { - fmt.Println("Algod is no longer running.") - os.Exit(0) - } - - fmt.Println("Failed to stop Algod.") - os.Exit(1) -} - -func stopAlgodProcess() { - // Check if algod is available as a systemd service - if checkSystemctlAlgorandServiceCreated() { - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) - } - - // Algod is available as a systemd service, stop it using systemctl - fmt.Println("Stopping algod using systemctl...") - cmd := exec.Command("systemctl", "stop", "algorand") - err := cmd.Run() - if err != nil { - fmt.Printf("Failed to stop algod service: %v\n", err) - cobra.CheckErr(err) - } - fmt.Println("Algod service stopped.") - } else { - // Algod is not available as a systemd service, stop it directly - fmt.Println("Stopping algod directly...") - // Find the process ID of algod - pid, err := findAlgodPID() - if err != nil { - fmt.Printf("Failed to find algod process: %v\n", err) - cobra.CheckErr(err) - } - - // Send SIGTERM to the process - process, err := os.FindProcess(pid) - if err != nil { - fmt.Printf("Failed to find process with PID %d: %v\n", pid, err) - cobra.CheckErr(err) - } - - err = process.Signal(syscall.SIGTERM) - if err != nil { - fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err) - cobra.CheckErr(err) - } - - fmt.Println("Sent SIGTERM to algod process.") - } -} - -// Upgrade ALGOD (if installed with package manager). -func upgradeAlgod() { - - if !isAlgodInstalled() { - fmt.Println("Algod is not installed. Please run the node install command.") - os.Exit(1) - } - - // Check if Algod was installed with apt/apt-get - if checkCmdToolExists("apt") { - upgradeDebianPackage("apt", "algorand-devtools") - } else if checkCmdToolExists("apt-get") { - upgradeDebianPackage("apt-get", "algorand-devtools") - } else if checkCmdToolExists("dnf") { - upgradeRpmPackage("dnf", "algorand-devtools") - } else if checkCmdToolExists("yum") { - upgradeRpmPackage("yum", "algorand-devtools") - } else { - fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.") - os.Exit(1) - } -} - -// Upgrade a package using the specified Debian package manager -func upgradeDebianPackage(packageManager, packageName string) { - // Check that we are calling with sudo - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) - } - - // Check if the package is installed and if there are updates available using apt-cache policy - cmd := exec.Command("apt-cache", "policy", packageName) - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Printf("Failed to check package policy: %v\n", err) - os.Exit(1) - } - - outputStr := string(output) - if strings.Contains(outputStr, "Installed: (none)") { - fmt.Printf("Package %s is not installed.\n", packageName) - os.Exit(1) - } - - installedVersion := extractVersion(outputStr, "Installed:") - candidateVersion := extractVersion(outputStr, "Candidate:") - - if installedVersion == candidateVersion { - fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion) - os.Exit(0) - } - - fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion) - - // Update the package list - fmt.Println("Updating package list...") - cmd = exec.Command(packageManager, "update") - err = cmd.Run() - if err != nil { - fmt.Printf("Failed to update package list: %v\n", err) - os.Exit(1) - } - - // Upgrade the package - fmt.Printf("Upgrading package %s...\n", packageName) - cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName) - err = cmd.Run() - if err != nil { - fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err) - os.Exit(1) - } - - fmt.Printf("Package %s upgraded successfully.\n", packageName) - os.Exit(0) -} - -// Upgrade a package using the specified RPM package manager -func upgradeRpmPackage(packageManager, packageName string) { - // Check that we are calling with sudo - if !isRunningWithSudo() { - fmt.Println("This command must be run with sudo.") - os.Exit(1) - } - - // Attempt to upgrade the package directly - fmt.Printf("Upgrading package %s...\n", packageName) - cmd := exec.Command(packageManager, "update", "-y", packageName) - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err) - os.Exit(1) - } - - outputStr := string(output) - if strings.Contains(outputStr, "Nothing to do") { - fmt.Printf("Package %s is already up-to-date.\n", packageName) - os.Exit(0) - } else { - fmt.Println(outputStr) - fmt.Printf("Package %s upgraded successfully.\n", packageName) - os.Exit(0) - } -} diff --git a/cmd/node/configure.go b/cmd/node/configure.go new file mode 100644 index 00000000..a8746d5b --- /dev/null +++ b/cmd/node/configure.go @@ -0,0 +1,330 @@ +package node + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "text/template" + + "github.com/spf13/cobra" +) + +var configureCmd = &cobra.Command{ + Use: "configure", + Short: "Configure Algod", + Long: "Configure Algod settings", + Run: func(cmd *cobra.Command, args []string) { + configureNode() + }, +} + +// TODO: configure not just data directory but algod path +func configureNode() { + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + panic("Unsupported OS: " + runtime.GOOS) + } + + var systemServiceConfigure bool + + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + // Check systemctl first + if checkAlgorandServiceCreated() { + if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") { + if checkAlgorandServiceActive() { + fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.") + os.Exit(1) + } + // Edit the service file with the user's new data directory + systemServiceConfigure = true + } else { + fmt.Println("Exiting...") + os.Exit(0) + } + } + + // At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file + // i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf + // ExecStart and Description will be changed to reflect the new data directory + // + + if !systemServiceConfigure { + fmt.Println("Configuring Data directory for algod started through Algorun...") + } + + algorandData := os.Getenv("ALGORAND_DATA") + + // Check if ALGORAND_DATA environment variable is set + if algorandData != "" { + fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData) + fmt.Println("Inspecting the set data directory...") + + if validateAlgorandDataDir(algorandData) { + fmt.Println("Found valid Algorand Data Directory: " + algorandData) + + if systemServiceConfigure { + if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") { + editAlgorandServiceFile(algorandData) + os.Exit(0) + } + } + + if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") { + fmt.Println("User chose not to set a completely new data directory.") + os.Exit(0) + } + + if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") { + newPath := promptWrapperInput("Enter the new data directory path") + + if !validateAlgorandDataDir(newPath) { + fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.") + os.Exit(1) + } + + if systemServiceConfigure { + // Edit the service file + editAlgorandServiceFile(newPath) + } else { + // Affect the ALGORAND_DATA environment variable + affectALGORAND_DATA(newPath) + } + os.Exit(0) + } + } else { + fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.") + } + } else { + fmt.Println("ALGORAND_DATA environment variable not set.") + } + + // Do quick "lazy" check for existing Algorand Data directories + paths := lazyCheckAlgorandDataDirs() + + if len(paths) != 0 { + + fmt.Println("Quick check found the following potential data directories:") + for _, path := range paths { + fmt.Println("✔ " + path) + } + + if len(paths) == 1 { + if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") { + if systemServiceConfigure { + // Edit the service file + editAlgorandServiceFile(paths[0]) + } else { + affectALGORAND_DATA(paths[0]) + } + os.Exit(0) + } + + } else { + + if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") { + + selectedPath := promptWrapperSelection("Select an Algorand data directory", paths) + + if systemServiceConfigure { + // Edit the service file + editAlgorandServiceFile(selectedPath) + } else { + affectALGORAND_DATA(selectedPath) + } + os.Exit(0) + } + } + } + + // Deep search + if promptWrapperNo("Do you want Algorun to do a deep search for pre-existing Algorand Data directories? (y/n)") { + fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...") + os.Exit(0) + } + + fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...") + paths = deepSearchAlgorandDataDirs() + + if len(paths) == 0 { + fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.") + os.Exit(1) + } + + fmt.Println("Found Algorand data directories:") + for _, path := range paths { + fmt.Println(path) + } + + // Prompt user to select a directory + selectedPath := promptWrapperSelection("Select an Algorand data directory", paths) + + if systemServiceConfigure { + editAlgorandServiceFile(selectedPath) + } else { + affectALGORAND_DATA(selectedPath) + } + os.Exit(0) +} + +func editAlgorandServiceFile(dataDirectoryPath string) { + switch runtime.GOOS { + case "linux": + editSystemdAlgorandServiceFile(dataDirectoryPath) + case "darwin": + editLaunchdAlgorandServiceFile(dataDirectoryPath) + default: + fmt.Println("Unsupported operating system.") + } +} + +func editLaunchdAlgorandServiceFile(dataDirectoryPath string) { + + algodPath, err := exec.LookPath("algod") + if err != nil { + fmt.Printf("Failed to find algod binary: %v\n", err) + os.Exit(1) + } + + overwriteFilePath := "/Library/LaunchDaemons/com.algorand.algod.plist" + + overwriteTemplate := ` + + + + Label + com.algorand.algod + ProgramArguments + + {{.AlgodPath}} + -d + {{.DataDirectoryPath}} + + RunAtLoad + + KeepAlive + + StandardOutPath + /tmp/algod.out + StandardErrorPath + /tmp/algod.err + + ` + + // Data to fill the template + data := map[string]string{ + "AlgodPath": algodPath, + "DataDirectoryPath": dataDirectoryPath, + } + + // Parse and execute the template + tmpl, err := template.New("override").Parse(overwriteTemplate) + if err != nil { + fmt.Printf("Failed to parse template: %v\n", err) + os.Exit(1) + } + + var overwriteContent bytes.Buffer + err = tmpl.Execute(&overwriteContent, data) + if err != nil { + fmt.Printf("Failed to execute template: %v\n", err) + os.Exit(1) + } + + // Write the override content to the file + err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644) + if err != nil { + fmt.Printf("Failed to write override file: %v\n", err) + os.Exit(1) + } + + // Boot out the launchd service (just in case - it should be off) + cmd := exec.Command("launchctl", "bootout", "system", overwriteFilePath) + err = cmd.Run() + if err != nil { + if !strings.Contains(err.Error(), "No such process") { + fmt.Printf("Failed to bootout launchd service: %v\n", err) + os.Exit(1) + } + } + + // Load the launchd service + cmd = exec.Command("launchctl", "load", overwriteFilePath) + err = cmd.Run() + if err != nil { + fmt.Printf("Failed to load launchd service: %v\n", err) + os.Exit(1) + } + + fmt.Println("Launchd service updated and reloaded successfully.") +} + +// Update the algorand.service file +func editSystemdAlgorandServiceFile(dataDirectoryPath string) { + + algodPath, err := exec.LookPath("algod") + if err != nil { + fmt.Printf("Failed to find algod binary: %v\n", err) + os.Exit(1) + } + + // Path to the systemd service override file + // Assuming that this is the same everywhere systemd is used + overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf" + + // Create the override directory if it doesn't exist + err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755) + if err != nil { + fmt.Printf("Failed to create override directory: %v\n", err) + os.Exit(1) + } + + // Content of the override file + const overrideTemplate = `[Unit] +Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}} +[Service] +ExecStart= +ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}` + + // Data to fill the template + data := map[string]string{ + "AlgodPath": algodPath, + "DataDirectoryPath": dataDirectoryPath, + } + + // Parse and execute the template + tmpl, err := template.New("override").Parse(overrideTemplate) + if err != nil { + fmt.Printf("Failed to parse template: %v\n", err) + os.Exit(1) + } + + var overrideContent bytes.Buffer + err = tmpl.Execute(&overrideContent, data) + if err != nil { + fmt.Printf("Failed to execute template: %v\n", err) + os.Exit(1) + } + + // Write the override content to the file + err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644) + if err != nil { + fmt.Printf("Failed to write override file: %v\n", err) + os.Exit(1) + } + + // Reload systemd manager configuration + cmd := exec.Command("systemctl", "daemon-reload") + err = cmd.Run() + if err != nil { + fmt.Printf("Failed to reload systemd daemon: %v\n", err) + os.Exit(1) + } + + fmt.Println("Algorand service file updated successfully.") +} diff --git a/cmd/node/install.go b/cmd/node/install.go new file mode 100644 index 00000000..e714e710 --- /dev/null +++ b/cmd/node/install.go @@ -0,0 +1,344 @@ +package node + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install Algorand node (Algod)", + Long: "Install Algorand node (Algod) and other binaries on your system", + Run: func(cmd *cobra.Command, args []string) { + installNode() + }, +} + +func installNode() { + fmt.Println("Checking if Algod is installed...") + + // Check if Algod is installed + if !isAlgodInstalled() { + fmt.Println("Algod is not installed. Installing...") + + // Install Algod based on OS + switch runtime.GOOS { + case "linux": + installNodeLinux() + case "darwin": + installNodeMac() + default: + panic("Unsupported OS: " + runtime.GOOS) + } + } else { + fmt.Println("Algod is already installed.") + printAlgodInfo() + } + +} + +func installNodeLinux() { + fmt.Println("Installing Algod on Linux") + + // Check that we are calling with sudo + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + var installCmds [][]string + var postInstallHint string + + // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager + + if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager + fmt.Println("Using apt package manager") + installCmds = [][]string{ + {"apt", "update"}, + {"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"}, + {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"}, + {"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`}, + {"apt", "update"}, + {"apt", "install", "-y", "algorand-devtools"}, + } + } else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get + fmt.Println("Using apt-get package manager") + installCmds = [][]string{ + {"apt-get", "update"}, + {"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"}, + {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"}, + {"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`}, + {"apt-get", "update"}, + {"apt-get", "install", "-y", "algorand-devtools"}, + } + } else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager + fmt.Println("Using dnf package manager") + installCmds = [][]string{ + {"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"}, + {"rpmkeys", "--import", "rpm_algorand.pub"}, + {"dnf", "install", "-y", "dnf-command(config-manager)"}, + {"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"}, + {"dnf", "install", "-y", "algorand-devtools"}, + {"systemctl", "start", "algorand"}, + } + } else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager + fmt.Println("Using yum package manager") + installCmds = [][]string{ + {"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"}, + {"rpmkeys", "--import", "rpm_algorand.pub"}, + {"yum", "install", "yum-utils"}, + {"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"}, + {"yum", "install", "-y", "algorand-devtools"}, + {"systemctl", "start", "algorand"}, + } + } else { + fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.") + installCmds = [][]string{ + {"mkdir", "~/node"}, + {"sh", "-c", "cd ~/node"}, + {"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"}, + {"chmod", "744", "update.sh"}, + {"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"}, + } + + postInstallHint = `You may need to add the Algorand binaries to your PATH: + export ALGORAND_DATA="$HOME/node/data" + export PATH="$HOME/node:$PATH" + ` + } + + // Run each installation command + for _, cmdArgs := range installCmds { + fmt.Println("Running command:", strings.Join(cmdArgs, " ")) + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err) + cobra.CheckErr(err) + } + } + + if postInstallHint != "" { + fmt.Println(postInstallHint) + } +} + +func installNodeMac() { + fmt.Println("Installing Algod on macOS...") + + // Check that we are calling with sudo + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user privileges (sudo).") + os.Exit(1) + } + + // Homebrew is our package manager of choice + if !checkCmdToolExists("brew") { + fmt.Println("Could not find Homebrew installed. Please install Homebrew and try again.") + os.Exit(1) + } + + originalUser := os.Getenv("SUDO_USER") + + // Run Homebrew commands as the original user without sudo + if err := runHomebrewInstallCommandsAsUser(originalUser); err != nil { + fmt.Printf("Homebrew commands failed: %v\n", err) + os.Exit(1) + } + + // Handle data directory and genesis.json file + handleDataDirMac() + + // Create and load the launchd service + createAndLoadLaunchdService() + + // Ensure Homebrew bin directory is in the PATH + // So that brew installed algorand binaries can be found + ensureHomebrewPathInEnv() + + if !isAlgodInstalled() { + fmt.Println("algod unexpectedly NOT in path. Installation failed.") + os.Exit(1) + } + + fmt.Println(`Installed Algorand (Algod) with Homebrew. +Algod is running in the background as a system-level service. + `) + os.Exit(0) +} + +func runHomebrewInstallCommandsAsUser(user string) error { + homebrewCmds := [][]string{ + {"brew", "tap", "HashMapsData2Value/homebrew-tap"}, + {"brew", "install", "algorand"}, + {"brew", "--prefix", "algorand", "--installed"}, + } + + for _, cmdArgs := range homebrewCmds { + fmt.Println("Running command:", strings.Join(cmdArgs, " ")) + cmd := exec.Command("sudo", append([]string{"-u", user}, cmdArgs...)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("command failed: %s\nError: %v", strings.Join(cmdArgs, " "), err) + } + } + return nil +} + +func handleDataDirMac() { + // Ensure the ~/.algorand directory exists + algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand") + if err := os.MkdirAll(algorandDir, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", algorandDir, err) + os.Exit(1) + } + + // Check if genesis.json file exists in ~/.algorand + genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json") + if _, err := os.Stat(genesisFilePath); os.IsNotExist(err) { + fmt.Println("genesis.json file does not exist. Downloading...") + + // Download the genesis.json file + resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json") + if err != nil { + fmt.Printf("Failed to download genesis.json: %v\n", err) + cobra.CheckErr(err) + } + defer resp.Body.Close() + + // Create the file + out, err := os.Create(genesisFilePath) + if err != nil { + fmt.Printf("Failed to create genesis.json file: %v\n", err) + cobra.CheckErr(err) + } + defer out.Close() + + // Write the content to the file + _, err = io.Copy(out, resp.Body) + if err != nil { + fmt.Printf("Failed to save genesis.json file: %v\n", err) + cobra.CheckErr(err) + } + + fmt.Println("mainnet genesis.json file downloaded successfully.") + } + +} + +func createAndLoadLaunchdService() { + // Get the prefix path for Algorand + cmd := exec.Command("brew", "--prefix", "algorand") + algorandPrefix, err := cmd.Output() + if err != nil { + fmt.Printf("Failed to get Algorand prefix: %v\n", err) + cobra.CheckErr(err) + } + algorandPrefixPath := strings.TrimSpace(string(algorandPrefix)) + + // Define the launchd plist content + plistContent := fmt.Sprintf(` + + + + Label + com.algorand.algod + ProgramArguments + + %s/bin/algod + -d + %s/.algorand + + RunAtLoad + + KeepAlive + + StandardOutPath + /tmp/algod.out + StandardErrorPath + /tmp/algod.err + +`, algorandPrefixPath, os.Getenv("HOME")) + + // Write the plist content to a file + plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist" + err = os.MkdirAll(filepath.Dir(plistPath), 0755) + if err != nil { + fmt.Printf("Failed to create LaunchDaemons directory: %v\n", err) + cobra.CheckErr(err) + } + + err = os.WriteFile(plistPath, []byte(plistContent), 0644) + if err != nil { + fmt.Printf("Failed to write plist file: %v\n", err) + cobra.CheckErr(err) + } + + // Load the launchd service + cmd = exec.Command("launchctl", "load", plistPath) + err = cmd.Run() + if err != nil { + fmt.Printf("Failed to load launchd service: %v\n", err) + cobra.CheckErr(err) + } + + // Check if the service is running + cmd = exec.Command("launchctl", "list", "com.algorand.algod") + err = cmd.Run() + if err != nil { + fmt.Printf("launchd service is not running: %v\n", err) + cobra.CheckErr(err) + } + + fmt.Println("Launchd service created and loaded successfully.") +} + +// Ensure that Homebrew bin directory is in the PATH so that Algorand binaries can be found +func ensureHomebrewPathInEnv() { + homebrewPrefix := os.Getenv("HOMEBREW_PREFIX") + homebrewCellar := os.Getenv("HOMEBREW_CELLAR") + homebrewRepository := os.Getenv("HOMEBREW_REPOSITORY") + + if homebrewPrefix == "" || homebrewCellar == "" || homebrewRepository == "" { + fmt.Println("Homebrew environment variables are not set. Running brew shellenv...") + + cmd := exec.Command("brew", "shellenv") + output, err := cmd.Output() + if err != nil { + fmt.Printf("Failed to get Homebrew environment: %v\n", err) + return + } + + envVars := strings.Split(string(output), "\n") + for _, envVar := range envVars { + if envVar != "" { + fmt.Println("Setting environment variable:", envVar) + os.Setenv(strings.Split(envVar, "=")[0], strings.Trim(strings.Split(envVar, "=")[1], `"`)) + } + } + + // Append brew shellenv output to .zshrc + zshrcPath := filepath.Join(os.Getenv("HOME"), ".zshrc") + f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("Failed to open .zshrc: %v\n", err) + fmt.Printf("Are you running a terminal other than zsh?") + fmt.Printf("Please run brew shellenv and add the output to your shell's configuration file.") + return + } + defer f.Close() + + if _, err := f.WriteString("\n# Inserted by Algorun\n# Homebrew environment variables\n" + string(output)); err != nil { + fmt.Printf("Failed to write to .zshrc: %v\n", err) + } + } +} diff --git a/cmd/node/main.go b/cmd/node/main.go new file mode 100644 index 00000000..ddf259ff --- /dev/null +++ b/cmd/node/main.go @@ -0,0 +1,21 @@ +package node + +import ( + "github.com/algorandfoundation/hack-tui/ui/style" + "github.com/spf13/cobra" +) + +var NodeCmd = &cobra.Command{ + Use: "node", + Short: "Algod installation", + Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("View the node status"), +} + +func init() { + NodeCmd.AddCommand(configureCmd) + NodeCmd.AddCommand(installCmd) + NodeCmd.AddCommand(startCmd) + NodeCmd.AddCommand(stopCmd) + NodeCmd.AddCommand(uninstallCmd) + NodeCmd.AddCommand(upgradeCmd) +} diff --git a/cmd/node/start.go b/cmd/node/start.go new file mode 100644 index 00000000..e7d4092d --- /dev/null +++ b/cmd/node/start.go @@ -0,0 +1,118 @@ +package node + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "syscall" + "time" + + "github.com/spf13/cobra" +) + +var startCmd = &cobra.Command{ + Use: "start", + Short: "Start Algod", + Long: "Start Algod on your system (the one on your PATH).", + Run: func(cmd *cobra.Command, args []string) { + startNode() + }, +} + +// Start Algod on your system (the one on your PATH). +func startNode() { + fmt.Println("Attempting to start Algod...") + + if !isAlgodInstalled() { + fmt.Println("Algod is not installed. Please run the *node install* command.") + os.Exit(1) + } + + // Check if Algod is already running + if isAlgodRunning() { + fmt.Println("Algod is already running.") + os.Exit(0) + } + + startAlgodProcess() +} + +func startAlgodProcess() { + + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + // Check if algod is available as a system service + if checkAlgorandServiceCreated() { + // Algod is available as a service + + switch runtime.GOOS { + case "linux": + startSystemdAlgorandService() + case "darwin": + startLaunchdAlgorandService() + default: // Unsupported OS + fmt.Println("Unsupported OS.") + os.Exit(1) + } + + } else { + // Algod is not available as a systemd service, start it directly + fmt.Println("Starting algod directly...") + + // Check if ALGORAND_DATA environment variable is set + fmt.Println("Checking if ALGORAND_DATA env var is set...") + algorandData := os.Getenv("ALGORAND_DATA") + + if !validateAlgorandDataDir(algorandData) { + fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.") + os.Exit(1) + } + + fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData) + + cmd := exec.Command("algod") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + err := cmd.Start() + if err != nil { + fmt.Printf("Failed to start algod: %v\n", err) + os.Exit(1) + } + } + + // Wait for the process to start + time.Sleep(5 * time.Second) + + if isAlgodRunning() { + fmt.Println("Algod is running.") + } else { + fmt.Println("Algod failed to start.") + } +} + +// Linux uses systemd +func startSystemdAlgorandService() { + fmt.Println("Starting algod using systemctl...") + cmd := exec.Command("systemctl", "start", "algorand") + err := cmd.Run() + if err != nil { + fmt.Printf("Failed to start algod service: %v\n", err) + os.Exit(1) + } +} + +// MacOS uses launchd instead of systemd +func startLaunchdAlgorandService() { + fmt.Println("Starting algod using launchctl...") + cmd := exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist") + err := cmd.Run() + if err != nil { + fmt.Printf("Failed to start algod service: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/node/stop.go b/cmd/node/stop.go new file mode 100644 index 00000000..fe5180ad --- /dev/null +++ b/cmd/node/stop.go @@ -0,0 +1,111 @@ +package node + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "syscall" + "time" + + "github.com/spf13/cobra" +) + +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "Stop Algod", + Long: "Stop the Algod process on your system.", + Run: func(cmd *cobra.Command, args []string) { + stopNode() + }, +} + +// Stop the Algod process on your system. +func stopNode() { + fmt.Println("Attempting to stop Algod...") + + if !isAlgodRunning() { + fmt.Println("Algod was not running.") + os.Exit(0) + } + + stopAlgodProcess() + + time.Sleep(5 * time.Second) + + if !isAlgodRunning() { + fmt.Println("Algod is no longer running.") + os.Exit(0) + } + + fmt.Println("Failed to stop Algod.") + os.Exit(1) +} + +func stopAlgodProcess() { + + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + // Check if algod is available as a system service + if checkAlgorandServiceCreated() { + switch runtime.GOOS { + case "linux": + stopSystemdAlgorandService() + case "darwin": + stopLaunchdAlgorandService() + default: // Unsupported OS + fmt.Println("Unsupported OS.") + os.Exit(1) + } + + } else { + // Algod is not available as a systemd service, stop it directly + fmt.Println("Stopping algod directly...") + // Find the process ID of algod + pid, err := findAlgodPID() + if err != nil { + fmt.Printf("Failed to find algod process: %v\n", err) + cobra.CheckErr(err) + } + + // Send SIGTERM to the process + process, err := os.FindProcess(pid) + if err != nil { + fmt.Printf("Failed to find process with PID %d: %v\n", pid, err) + cobra.CheckErr(err) + } + + err = process.Signal(syscall.SIGTERM) + if err != nil { + fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err) + cobra.CheckErr(err) + } + + fmt.Println("Sent SIGTERM to algod process.") + } +} + +func stopLaunchdAlgorandService() { + fmt.Println("Stopping algod using launchd...") + cmd := exec.Command("launchctl", "bootout", "system/com.algorand.algod") + err := cmd.Run() + if err != nil { + fmt.Printf("Failed to stop algod service: %v\n", err) + cobra.CheckErr(err) + } + fmt.Println("Algod service stopped.") +} + +func stopSystemdAlgorandService() { + fmt.Println("Stopping algod using systemctl...") + cmd := exec.Command("systemctl", "stop", "algorand") + err := cmd.Run() + if err != nil { + fmt.Printf("Failed to stop algod service: %v\n", err) + cobra.CheckErr(err) + } + fmt.Println("Algod service stopped.") +} diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go new file mode 100644 index 00000000..e20d3c74 --- /dev/null +++ b/cmd/node/uninstall.go @@ -0,0 +1,99 @@ +package node + +import ( + "fmt" + "os" + "os/exec" + "runtime" + + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Uninstall Algorand node (Algod)", + Long: "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.", + Run: func(cmd *cobra.Command, args []string) { + unInstallNode() + }, +} + +// Uninstall Algorand node (Algod) and other binaries on your system +func unInstallNode() { + + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + fmt.Println("Checking if Algod is installed...") + + // Check if Algod is installed + if !isAlgodInstalled() { + fmt.Println("Algod is not installed.") + os.Exit(0) + } + + fmt.Println("Algod is installed. Uninstalling...") + + // Check if Algod is running + if isAlgodRunning() { + fmt.Println("Algod is running. Please run *node stop*.") + os.Exit(1) + } + + // Uninstall Algod based on OS + switch runtime.GOOS { + case "linux": + unInstallNodeLinux() + case "darwin": + unInstallNodeMac() + default: + panic("Unsupported OS: " + runtime.GOOS) + } + + os.Exit(0) +} + +func unInstallNodeMac() { + fmt.Println("Uninstalling Algod on macOS...") + + // Homebrew is our package manager of choice + if !checkCmdToolExists("brew") { + fmt.Println("Could not find Homebrew installed. Did you install Algod some other way?.") + os.Exit(1) + } + + user := os.Getenv("SUDO_USER") + + // Run the brew uninstall command as the original user without sudo + cmd := exec.Command("sudo", "-u", user, "brew", "uninstall", "algorand", "--formula") + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("Failed to uninstall Algorand: %v\n", err) + fmt.Printf("Output: %s\n", string(output)) + os.Exit(1) + } + + fmt.Printf("Output: %s\n", string(output)) + + // Calling brew uninstall algorand without sudo user privileges + cmd = exec.Command("sudo", "-u", user, "brew", "--prefix", "algorand", "--installed") + err = cmd.Run() + if err == nil { + fmt.Println("Algorand uninstall failed.") + os.Exit(1) + } + + // Delete the launchd plist file + plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist" + err = os.Remove(plistPath) + if err != nil { + fmt.Printf("Failed to delete plist file: %v\n", err) + os.Exit(1) + } + + fmt.Println("Algorand uninstalled successfully.") +} + +func unInstallNodeLinux() {} diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go new file mode 100644 index 00000000..55210b09 --- /dev/null +++ b/cmd/node/upgrade.go @@ -0,0 +1,175 @@ +package node + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +var upgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "Upgrade Algod", + Long: "Upgrade Algod (if installed with package manager).", + Run: func(cmd *cobra.Command, args []string) { + upgradeAlgod() + }, +} + +// Upgrade ALGOD (if installed with package manager). +func upgradeAlgod() { + if !isAlgodInstalled() { + fmt.Println("Algod is not installed.") + os.Exit(1) + } + + switch runtime.GOOS { + case "darwin": + if checkCmdToolExists("brew") { + upgradeBrewAlgorand() + } else { + fmt.Println("Homebrew is not installed. Please install Homebrew and try again.") + os.Exit(1) + } + case "linux": + // Check if Algod was installed with apt/apt-get, dnf, or yum + if checkCmdToolExists("apt") { + upgradeDebianPackage("apt", "algorand-devtools") + } else if checkCmdToolExists("apt-get") { + upgradeDebianPackage("apt-get", "algorand-devtools") + } else if checkCmdToolExists("dnf") { + upgradeRpmPackage("dnf", "algorand-devtools") + } else if checkCmdToolExists("yum") { + upgradeRpmPackage("yum", "algorand-devtools") + } else { + fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.") + os.Exit(1) + } + default: + fmt.Println("Unsupported operating system. The *node upgrade* command is only available for macOS and Linux.") + os.Exit(1) + } +} + +func upgradeBrewAlgorand() { + fmt.Println("Upgrading Algod using Homebrew...") + + var prefixCommand []string + + // Brew cannot be run with sudo, so we need to run the commands as the original user. + // This checks if the user has ran this command with super-user privileges, and if so + // counteracts it by running the commands as the original user. + if isRunningWithSudo() { + originalUser := os.Getenv("SUDO_USER") + prefixCommand = []string{"sudo", "-u", originalUser} + } else { + prefixCommand = []string{} + } + + // Check if algorand is installed with Homebrew + checkCmdArgs := append(prefixCommand, "brew", "--prefix", "algorand", "--installed") + fmt.Println("Running command:", strings.Join(checkCmdArgs, " ")) + checkCmd := exec.Command(checkCmdArgs[0], checkCmdArgs[1:]...) + checkCmd.Stdout = os.Stdout + checkCmd.Stderr = os.Stderr + if err := checkCmd.Run(); err != nil { + fmt.Println("Algorand is not installed with Homebrew.") + os.Exit(1) + } + + // Upgrade algorand + upgradeCmdArgs := append(prefixCommand, "brew", "upgrade", "algorand", "--formula") + fmt.Println("Running command:", strings.Join(upgradeCmdArgs, " ")) + upgradeCmd := exec.Command(upgradeCmdArgs[0], upgradeCmdArgs[1:]...) + upgradeCmd.Stdout = os.Stdout + upgradeCmd.Stderr = os.Stderr + if err := upgradeCmd.Run(); err != nil { + fmt.Printf("Failed to upgrade Algorand: %v\n", err) + os.Exit(1) + } +} + +// Upgrade a package using the specified Debian package manager +func upgradeDebianPackage(packageManager, packageName string) { + // Check that we are calling with sudo + if !isRunningWithSudo() { + fmt.Println("This command must be run with super-user priviledges (sudo).") + os.Exit(1) + } + + // Check if the package is installed and if there are updates available using apt-cache policy + cmd := exec.Command("apt-cache", "policy", packageName) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("Failed to check package policy: %v\n", err) + os.Exit(1) + } + + outputStr := string(output) + if strings.Contains(outputStr, "Installed: (none)") { + fmt.Printf("Package %s is not installed.\n", packageName) + os.Exit(1) + } + + installedVersion := extractVersion(outputStr, "Installed:") + candidateVersion := extractVersion(outputStr, "Candidate:") + + if installedVersion == candidateVersion { + fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion) + os.Exit(0) + } + + fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion) + + // Update the package list + fmt.Println("Updating package list...") + cmd = exec.Command(packageManager, "update") + err = cmd.Run() + if err != nil { + fmt.Printf("Failed to update package list: %v\n", err) + os.Exit(1) + } + + // Upgrade the package + fmt.Printf("Upgrading package %s...\n", packageName) + cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName) + err = cmd.Run() + if err != nil { + fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err) + os.Exit(1) + } + + fmt.Printf("Package %s upgraded successfully.\n", packageName) + os.Exit(0) +} + +// Upgrade a package using the specified RPM package manager +func upgradeRpmPackage(packageManager, packageName string) { + // Check that we are calling with sudo + if !isRunningWithSudo() { + fmt.Println("This command must be run with sudo.") + os.Exit(1) + } + + // Attempt to upgrade the package directly + fmt.Printf("Upgrading package %s...\n", packageName) + cmd := exec.Command(packageManager, "update", "-y", packageName) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err) + os.Exit(1) + } + + outputStr := string(output) + if strings.Contains(outputStr, "Nothing to do") { + fmt.Printf("Package %s is already up-to-date.\n", packageName) + os.Exit(0) + } else { + fmt.Println(outputStr) + fmt.Printf("Package %s upgraded successfully.\n", packageName) + os.Exit(0) + } +} diff --git a/cmd/utils.go b/cmd/node/utils.go similarity index 75% rename from cmd/utils.go rename to cmd/node/utils.go index b3c56acf..e15763e1 100644 --- a/cmd/utils.go +++ b/cmd/node/utils.go @@ -1,4 +1,4 @@ -package cmd +package node import ( "bytes" @@ -9,7 +9,6 @@ import ( "runtime" "strings" "sync" - "text/template" "github.com/manifoldco/promptui" "github.com/spf13/cobra" @@ -107,73 +106,6 @@ func affectALGORAND_DATA(path string) { fmt.Println("") } -// Update the algorand.service file -func editAlgorandServiceFile(dataDirectoryPath string) { - - // TODO: look into setting algod path as well as the data directory path - // Find the path to the algod binary - algodPath, err := exec.LookPath("algod") - if err != nil { - fmt.Printf("Failed to find algod binary: %v\n", err) - os.Exit(1) - } - - // Path to the systemd service override file - // Assuming that this is the same everywhere systemd is used - overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf" - - // Create the override directory if it doesn't exist - err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755) - if err != nil { - fmt.Printf("Failed to create override directory: %v\n", err) - os.Exit(1) - } - - // Content of the override file - const overrideTemplate = `[Unit] -Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}} -[Service] -ExecStart= -ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}` - - // Data to fill the template - data := map[string]string{ - "AlgodPath": algodPath, - "DataDirectoryPath": dataDirectoryPath, - } - - // Parse and execute the template - tmpl, err := template.New("override").Parse(overrideTemplate) - if err != nil { - fmt.Printf("Failed to parse template: %v\n", err) - os.Exit(1) - } - - var overrideContent bytes.Buffer - err = tmpl.Execute(&overrideContent, data) - if err != nil { - fmt.Printf("Failed to execute template: %v\n", err) - os.Exit(1) - } - - // Write the override content to the file - err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644) - if err != nil { - fmt.Printf("Failed to write override file: %v\n", err) - os.Exit(1) - } - - // Reload systemd manager configuration - cmd := exec.Command("systemctl", "daemon-reload") - err = cmd.Run() - if err != nil { - fmt.Printf("Failed to reload systemd daemon: %v\n", err) - os.Exit(1) - } - - fmt.Println("Algorand service file updated successfully.") -} - // Check if the program is running with admin (super-user) priviledges func isRunningWithSudo() bool { return os.Geteuid() == 0 @@ -304,8 +236,21 @@ func findAlgodPID() (int, error) { return pid, nil } -// Check systemctl has Algorand Service been created in the first place -func checkSystemctlAlgorandServiceCreated() bool { +// Check if Algorand service has been created +func checkAlgorandServiceCreated() bool { + switch runtime.GOOS { + case "linux": + return checkSystemdAlgorandServiceCreated() + case "darwin": + return checkLaunchdAlgorandServiceCreated() + default: + fmt.Println("Unsupported operating system.") + return false + } +} + +// Check if Algorand service has been created with systemd (Linux) +func checkSystemdAlgorandServiceCreated() bool { cmd := exec.Command("systemctl", "list-unit-files", "algorand.service") var out bytes.Buffer cmd.Stdout = &out @@ -316,7 +261,40 @@ func checkSystemctlAlgorandServiceCreated() bool { return strings.Contains(out.String(), "algorand.service") } -func checkSystemctlAlgorandServiceActive() bool { +// Check if Algorand service has been created with launchd (macOS) +// Note that it needs to be run in super-user privilege mode to +// be able to view the root level services. +func checkLaunchdAlgorandServiceCreated() bool { + cmd := exec.Command("launchctl", "list", "com.algorand.algod") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + output := out.String() + if err != nil { + fmt.Printf("Failed to check launchd service: %v\n", err) + return false + } + + if strings.Contains(output, "Could not find service") { + return false + } + + return true +} + +func checkAlgorandServiceActive() bool { + switch runtime.GOOS { + case "linux": + return checkSystemdAlgorandServiceActive() + case "darwin": + return checkLaunchdAlgorandServiceActive() + default: + fmt.Println("Unsupported operating system.") + return false + } +} + +func checkSystemdAlgorandServiceActive() bool { cmd := exec.Command("systemctl", "is-active", "algorand") var out bytes.Buffer cmd.Stdout = &out @@ -327,6 +305,22 @@ func checkSystemctlAlgorandServiceActive() bool { return strings.TrimSpace(out.String()) == "active" } +func checkLaunchdAlgorandServiceActive() bool { + cmd := exec.Command("launchctl", "print", "system/com.algorand.algod") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + output := out.String() + if err != nil { + return false + } + if strings.Contains(output, "Bad request") || strings.Contains(output, "Could not find service") { + return false + } + + return true +} + // Extract version information from apt-cache policy output func extractVersion(output, prefix string) string { lines := strings.Split(output, "\n") @@ -338,3 +332,10 @@ func extractVersion(output, prefix string) string { } return "" } + +func isAlgodRunning() bool { + // Check if Algod is already running + // This works for systemctl started algorand.service as well as directly started algod + err := exec.Command("pgrep", "algod").Run() + return err == nil +} diff --git a/cmd/root.go b/cmd/root.go index f50bc7a0..ae69ec23 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,7 +4,12 @@ import ( "context" "encoding/json" "fmt" + "io" + "os" + "strings" + "github.com/algorandfoundation/hack-tui/api" + "github.com/algorandfoundation/hack-tui/cmd/node" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui" "github.com/algorandfoundation/hack-tui/ui/style" @@ -13,20 +18,8 @@ import ( "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" "github.com/spf13/cobra" "github.com/spf13/viper" - "io" - "os" - "strings" ) -const BANNER = ` - _____ .__ __________ - / _ \ | | ____ ____\______ \__ __ ____ - / /_\ \| | / ___\ / _ \| _/ | \/ \ -/ | \ |__/ /_/ > <_> ) | \ | / | \ -\____|__ /____/\___ / \____/|____|_ /____/|___| / - \/ /_____/ \/ \/ -` - var ( server string token = strings.Repeat("a", 64) @@ -34,7 +27,7 @@ var ( rootCmd = &cobra.Command{ Use: "algorun", Short: "Manage Algorand nodes", - Long: style.Purple(BANNER) + "\n", + Long: style.Purple(style.BANNER) + "\n", CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, @@ -145,6 +138,7 @@ func init() { // Add Commands rootCmd.AddCommand(statusCmd) + rootCmd.AddCommand(node.NodeCmd) } // Execute executes the root command. diff --git a/cmd/root_test.go b/cmd/root_test.go index 7cae6f28..2fcf57f8 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/spf13/viper" "os" "testing" + + "github.com/spf13/viper" ) // Test the stub root command diff --git a/cmd/status.go b/cmd/status.go index 5af7f38b..286a14ee 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -4,20 +4,21 @@ import ( "context" "errors" "fmt" + "os" + "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui" "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" "github.com/spf13/viper" - "os" ) // statusCmd is the main entrypoint for the `status` cobra.Command with a tea.Program var statusCmd = &cobra.Command{ Use: "status", Short: "Get the node status", - Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"), + Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("View the node status"), RunE: func(cmd *cobra.Command, args []string) error { initConfig() if viper.GetString("server") == "" { diff --git a/internal/accounts.go b/internal/accounts.go index 7169f828..b7492b03 100644 --- a/internal/accounts.go +++ b/internal/accounts.go @@ -121,18 +121,23 @@ func AccountsFromState(state *StateModel, t Time, client *api.ClientWithResponse for _, key := range *state.ParticipationKeys { val, ok := values[key.Address] if !ok { - - account, err := GetAccount(client, key.Address) - - // TODO: handle error - if err != nil { - // TODO: Logging - panic(err) + var account = api.Account{ + Address: key.Address, + Status: "Unknown", + Amount: 0, } - - var expires = t.Now() + if state.Status.State != "SYNCING" { + var err error + account, err = GetAccount(client, key.Address) + // TODO: handle error + if err != nil { + // TODO: Logging + panic(err) + } + } + now := t.Now() + var expires = now.Add(-(time.Hour * 24 * 365 * 100)) if key.EffectiveLastValid != nil { - now := t.Now() roundDiff := max(0, *key.EffectiveLastValid-int(state.Status.LastRound)) distance := int(state.Metrics.RoundTime) * roundDiff expires = now.Add(time.Duration(distance)) diff --git a/internal/state.go b/internal/state.go index e14c07aa..7b3ccd8b 100644 --- a/internal/state.go +++ b/internal/state.go @@ -62,6 +62,11 @@ func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Co // Fetch Keys s.UpdateKeys(ctx, client) + if s.Status.State == "SYNCING" { + lastRound = s.Status.LastRound + cb(s, nil) + continue + } // Run Round Averages and RX/TX every 5 rounds if s.Status.LastRound%5 == 0 { bm, err := GetBlockMetrics(ctx, client, s.Status.LastRound, s.Metrics.Window) diff --git a/ui/pages/accounts/controller.go b/ui/pages/accounts/controller.go index 139ee6dc..4e4cf942 100644 --- a/ui/pages/accounts/controller.go +++ b/ui/pages/accounts/controller.go @@ -21,7 +21,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { switch msg := msg.(type) { case internal.StateModel: - m.Data = msg.Accounts + m.Data = &msg m.table.SetRows(*m.makeRows()) case tea.KeyMsg: switch msg.String() { diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go index 425adf40..91b550cb 100644 --- a/ui/pages/accounts/model.go +++ b/ui/pages/accounts/model.go @@ -4,6 +4,7 @@ import ( "github.com/algorandfoundation/hack-tui/ui/style" "sort" "strconv" + "time" "github.com/algorandfoundation/hack-tui/internal" "github.com/charmbracelet/bubbles/table" @@ -13,7 +14,7 @@ import ( type ViewModel struct { Width int Height int - Data map[string]internal.Account + Data *internal.StateModel table table.Model navigation string @@ -24,7 +25,7 @@ func New(state *internal.StateModel) ViewModel { m := ViewModel{ Width: 0, Height: 0, - Data: state.Accounts, + Data: state, controls: "( (g)enerate )", navigation: "| " + style.Green.Render("(a)ccounts") + " | (k)eys | (t)xn |", } @@ -52,7 +53,7 @@ func (m ViewModel) SelectedAccount() internal.Account { var account internal.Account var selectedRow = m.table.SelectedRow() if selectedRow != nil { - account = m.Data[selectedRow[0]] + account = m.Data.Accounts[selectedRow[0]] } return account } @@ -70,13 +71,20 @@ func (m ViewModel) makeColumns(width int) []table.Column { func (m ViewModel) makeRows() *[]table.Row { rows := make([]table.Row, 0) - for key := range m.Data { + for key := range m.Data.Accounts { + expires := m.Data.Accounts[key].Expires.String() + if m.Data.Status.State == "SYNCING" { + expires = "SYNCING" + } + if !m.Data.Accounts[key].Expires.After(time.Now().Add(-(time.Hour * 24 * 365 * 50))) { + expires = "NA" + } rows = append(rows, table.Row{ - m.Data[key].Address, - strconv.Itoa(m.Data[key].Keys), - m.Data[key].Status, - m.Data[key].Expires.String(), - strconv.Itoa(m.Data[key].Balance), + m.Data.Accounts[key].Address, + strconv.Itoa(m.Data.Accounts[key].Keys), + m.Data.Accounts[key].Status, + expires, + strconv.Itoa(m.Data.Accounts[key].Balance), }) } sort.SliceStable(rows, func(i, j int) bool { diff --git a/ui/status.go b/ui/status.go index 451a5758..ade98da4 100644 --- a/ui/status.go +++ b/ui/status.go @@ -2,14 +2,15 @@ package ui import ( "fmt" - "github.com/algorandfoundation/hack-tui/internal" - "github.com/algorandfoundation/hack-tui/ui/style" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "math" "strconv" "strings" "time" + + "github.com/algorandfoundation/hack-tui/internal" + "github.com/algorandfoundation/hack-tui/ui/style" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // StatusViewModel is extended from the internal.StatusModel @@ -85,14 +86,22 @@ func (m StatusViewModel) View() string { // Last Round row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) - beginning = style.Blue.Render(" Round time: ") + fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second)) - end = getBitRate(m.Data.Metrics.TX) + style.Green.Render("TX ") + roundTime := fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second)) + if m.Data.Status.State == "SYNCING" { + roundTime = "--" + } + beginning = style.Blue.Render(" Round time: ") + roundTime + end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + style.Green.Render("TX ") middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2))) row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) - beginning = style.Blue.Render(" TPS: ") + fmt.Sprintf("%.2f", m.Data.Metrics.TPS) - end = getBitRate(m.Data.Metrics.RX) + style.Green.Render("RX ") + tps := fmt.Sprintf("%.2f", m.Data.Metrics.TPS) + if m.Data.Status.State == "SYNCING" { + tps = "--" + } + beginning = style.Blue.Render(" TPS: ") + tps + end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + style.Green.Render("RX ") middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2))) row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) diff --git a/ui/style/style.go b/ui/style/style.go index 1647f3f7..dcb5fdb2 100644 --- a/ui/style/style.go +++ b/ui/style/style.go @@ -1,8 +1,9 @@ package style import ( - "github.com/charmbracelet/lipgloss" "strings" + + "github.com/charmbracelet/lipgloss" ) var ( @@ -73,3 +74,12 @@ func WithNavigation(controls string, view string) string { } return view } + +const BANNER = ` + _____ .__ __________ + / _ \ | | ____ ____\______ \__ __ ____ + / /_\ \| | / ___\ / _ \| _/ | \/ \ +/ | \ |__/ /_/ > <_> ) | \ | / | \ +\____|__ /____/\___ / \____/|____|_ /____/|___| / + \/ /_____/ \/ \/ +` diff --git a/ui/viewport.go b/ui/viewport.go index 660c73cf..9f87aa8b 100644 --- a/ui/viewport.go +++ b/ui/viewport.go @@ -123,7 +123,7 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.page == KeysPage { selKey := m.keysPage.SelectedKey() - if selKey != nil { + if selKey != nil && m.Data.Status.State != "SYNCING" { m.page = TransactionPage return m, keys.EmitKeySelected(selKey) } @@ -143,21 +143,24 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil case "t": - if m.page == AccountsPage { - acct := m.accountsPage.SelectedAccount() - data := *m.Data.ParticipationKeys - for i, key := range data { - if key.Address == acct.Address { - m.page = TransactionPage - return m, keys.EmitKeySelected(&data[i]) + if m.Data.Status.State != "SYNCING" { + + if m.page == AccountsPage { + acct := m.accountsPage.SelectedAccount() + data := *m.Data.ParticipationKeys + for i, key := range data { + if key.Address == acct.Address { + m.page = TransactionPage + return m, keys.EmitKeySelected(&data[i]) + } } } - } - if m.page == KeysPage { - selKey := m.keysPage.SelectedKey() - if selKey != nil { - m.page = TransactionPage - return m, keys.EmitKeySelected(selKey) + if m.page == KeysPage { + selKey := m.keysPage.SelectedKey() + if selKey != nil { + m.page = TransactionPage + return m, keys.EmitKeySelected(selKey) + } } } return m, nil