From ea7c2b331889d6d7adb32eac00856abc2ce17671 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:23:32 +0100 Subject: [PATCH 01/23] # This is a combination of 39 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 --- cmd/node.go | 609 ++++++++++++++++++ cmd/root.go | 78 ++- cmd/root_test.go | 54 +- cmd/status.go | 1 + .../{ => Test_InitConfig}/algod.admin.token | 0 .../{ => Test_InitConfig}/config.json | 0 .../algod.admin.token | 1 + .../Test_InitConfigWithAddress/config.json | 4 + .../algod.admin.token | 1 + .../config.json | 4 + .../algod.admin.token | 1 + .../Test_InitConfigWithoutEndpoint/algod.net | 1 + .../config.json | 2 + cmd/utils.go | 340 ++++++++++ go.mod | 2 + go.sum | 9 + internal/metrics.go | 3 + internal/state.go | 11 +- ui/status.go | 20 +- 19 files changed, 1121 insertions(+), 20 deletions(-) create mode 100644 cmd/node.go rename cmd/testdata/{ => Test_InitConfig}/algod.admin.token (100%) rename cmd/testdata/{ => Test_InitConfig}/config.json (100%) create mode 100644 cmd/testdata/Test_InitConfigWithAddress/algod.admin.token create mode 100644 cmd/testdata/Test_InitConfigWithAddress/config.json create mode 100644 cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token create mode 100644 cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/config.json create mode 100644 cmd/utils.go diff --git a/cmd/node.go b/cmd/node.go new file mode 100644 index 00000000..bb3bd6e5 --- /dev/null +++ b/cmd/node.go @@ -0,0 +1,609 @@ +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/root.go b/cmd/root.go index 1f04e8e9..f50bc7a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "context" "encoding/json" + "fmt" "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui" @@ -39,11 +40,24 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { log.SetOutput(cmd.OutOrStdout()) + initConfig() + + if viper.GetString("server") == "" { + return fmt.Errorf(style.Red.Render("server is required")) + } + if viper.GetString("token") == "" { + return fmt.Errorf(style.Red.Render("token is required")) + } + client, err := getClient() cobra.CheckErr(err) partkeys, err := internal.GetPartKeys(context.Background(), client) - cobra.CheckErr(err) + if err != nil { + return fmt.Errorf( + style.Red.Render("failed to get participation keys: %s"), + err) + } state := internal.StateModel{ Status: internal.StatusModel{ @@ -106,7 +120,7 @@ func check(err interface{}) { // Handle global flags and set usage templates func init() { log.SetReportTimestamp(false) - initConfig() + // Configure Version if Version == "" { Version = "unknown (built from source)" @@ -142,6 +156,15 @@ type AlgodConfig struct { EndpointAddress string `json:"EndpointAddress"` } +func replaceEndpointUrl(s string) string { + s = strings.Replace(s, "\n", "", 1) + s = strings.Replace(s, "0.0.0.0", "127.0.0.1", 1) + s = strings.Replace(s, "[::]", "127.0.0.1", 1) + return s +} +func hasWildcardEndpointUrl(s string) bool { + return strings.Contains(s, "0.0.0.0") || strings.Contains(s, "::") +} func initConfig() { // Find home directory. home, err := os.UserHomeDir() @@ -159,12 +182,17 @@ func initConfig() { // Load Configurations viper.AutomaticEnv() - err = viper.ReadInConfig() + _ = viper.ReadInConfig() + + // Check for server + loadedServer := viper.GetString("server") + loadedToken := viper.GetString("token") + // Load ALGORAND_DATA/config.json algorandData, exists := os.LookupEnv("ALGORAND_DATA") // Load the Algorand Data Configuration - if exists && algorandData != "" { + if exists && algorandData != "" && loadedServer == "" { // Placeholder for Struct var algodConfig AlgodConfig @@ -183,23 +211,43 @@ func initConfig() { err = configFile.Close() check(err) - // Replace catchall address with localhost - if strings.Contains(algodConfig.EndpointAddress, "0.0.0.0") { - algodConfig.EndpointAddress = strings.Replace(algodConfig.EndpointAddress, "0.0.0.0", "127.0.0.1", 1) + // Check for endpoint address + if hasWildcardEndpointUrl(algodConfig.EndpointAddress) { + algodConfig.EndpointAddress = replaceEndpointUrl(algodConfig.EndpointAddress) + } else if algodConfig.EndpointAddress == "" { + // Assume it is not set, try to discover the port from the network file + networkPath := algorandData + "/algod.net" + networkFile, err := os.Open(networkPath) + check(err) + + byteValue, err = io.ReadAll(networkFile) + check(err) + + if hasWildcardEndpointUrl(string(byteValue)) { + algodConfig.EndpointAddress = replaceEndpointUrl(string(byteValue)) + } else { + algodConfig.EndpointAddress = string(byteValue) + } + } + if strings.Contains(algodConfig.EndpointAddress, ":0") { + algodConfig.EndpointAddress = strings.Replace(algodConfig.EndpointAddress, ":0", ":8080", 1) + } + if loadedToken == "" { + // Handle Token Path + tokenPath := algorandData + "/algod.admin.token" - // Handle Token Path - tokenPath := algorandData + "/algod.admin.token" + tokenFile, err := os.Open(tokenPath) + check(err) - tokenFile, err := os.Open(tokenPath) - check(err) + byteValue, err = io.ReadAll(tokenFile) + check(err) - byteValue, err = io.ReadAll(tokenFile) - check(err) + viper.Set("token", strings.Replace(string(byteValue), "\n", "", 1)) + } // Set the server configuration - viper.Set("server", "http://"+algodConfig.EndpointAddress) - viper.Set("token", string(byteValue)) + viper.Set("server", "http://"+strings.Replace(algodConfig.EndpointAddress, "\n", "", 1)) viper.Set("data", dataConfigPath) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 0b7b8b7e..7cae6f28 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -21,12 +21,64 @@ func Test_ExecuteRootCommand(t *testing.T) { func Test_InitConfig(t *testing.T) { cwd, _ := os.Getwd() - t.Setenv("ALGORAND_DATA", cwd+"/testdata") + viper.Set("token", "") + viper.Set("server", "") + t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfig") initConfig() server := viper.Get("server") if server == "" { t.Fatal("Invalid Server") } + if server != "http://127.0.0.1:8080" { + t.Fatal("Invalid Server") + } +} + +func Test_InitConfigWithoutEndpoint(t *testing.T) { + cwd, _ := os.Getwd() + viper.Set("token", "") + viper.Set("server", "") + t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithoutEndpoint") + + initConfig() + server := viper.Get("server") + if server == "" { + t.Fatal("Invalid Server") + } + if server != "http://127.0.0.1:8080" { + t.Fatal("Invalid Server") + } +} + +func Test_InitConfigWithAddress(t *testing.T) { + cwd, _ := os.Getwd() + viper.Set("token", "") + viper.Set("server", "") + t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddress") + + initConfig() + server := viper.Get("server") + if server == "" { + t.Fatal("Invalid Server") + } + if server != "http://255.255.255.255:8080" { + t.Fatal("Invalid Server") + } +} +func Test_InitConfigWithAddressAndDefaultPort(t *testing.T) { + cwd, _ := os.Getwd() + viper.Set("token", "") + viper.Set("server", "") + t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddressAndDefaultPort") + + initConfig() + server := viper.Get("server") + if server == "" { + t.Fatal("Invalid Server") + } + if server != "http://255.255.255.255:8080" { + t.Fatal("Invalid Server") + } } diff --git a/cmd/status.go b/cmd/status.go index 3bff01e3..5af7f38b 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -19,6 +19,7 @@ var statusCmd = &cobra.Command{ Short: "Get the node status", Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"), RunE: func(cmd *cobra.Command, args []string) error { + initConfig() if viper.GetString("server") == "" { return errors.New(style.Magenta("server is required")) } diff --git a/cmd/testdata/algod.admin.token b/cmd/testdata/Test_InitConfig/algod.admin.token similarity index 100% rename from cmd/testdata/algod.admin.token rename to cmd/testdata/Test_InitConfig/algod.admin.token diff --git a/cmd/testdata/config.json b/cmd/testdata/Test_InitConfig/config.json similarity index 100% rename from cmd/testdata/config.json rename to cmd/testdata/Test_InitConfig/config.json diff --git a/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token b/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token new file mode 100644 index 00000000..71b7a719 --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithAddress/config.json b/cmd/testdata/Test_InitConfigWithAddress/config.json new file mode 100644 index 00000000..f172cd52 --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithAddress/config.json @@ -0,0 +1,4 @@ +{ + "EndpointAddress": "255.255.255.255:8080", + "OtherKey": "" +} \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token new file mode 100644 index 00000000..71b7a719 --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json new file mode 100644 index 00000000..0e455f6b --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json @@ -0,0 +1,4 @@ +{ + "EndpointAddress": "255.255.255.255:0", + "OtherKey": "" +} \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token new file mode 100644 index 00000000..71b7a719 --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net new file mode 100644 index 00000000..7985b074 --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net @@ -0,0 +1 @@ +[::]:8080 \ No newline at end of file diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json b/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 00000000..b3c56acf --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,340 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "text/template" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +type Release struct { + Name string `json:"name"` + ZipballURL string `json:"zipball_url"` + TarballURL string `json:"tarball_url"` + Commit struct { + Sha string `json:"sha"` + URL string `json:"url"` + } `json:"commit"` + NodeID string `json:"node_id"` +} + +// Queries user on the provided prompt and returns the user input +func promptWrapperInput(promptLabel string) string { + prompt := promptui.Prompt{ + Label: promptLabel, + } + + result, err := prompt.Run() + cobra.CheckErr(err) + + return result +} + +// Queries user on the provided prompt and returns true if user inputs "y" +func promptWrapperYes(promptLabel string) bool { + return promptWrapperInput(promptLabel) == "y" +} + +// Queries user on the provided prompt and returns true if user does not input "y" +// Included for improved readability of decision tree, despite being redundant. +func promptWrapperNo(promptLabel string) bool { + return promptWrapperInput(promptLabel) != "y" +} + +// Queries user on the provided prompt and returns the selected item +func promptWrapperSelection(promptLabel string, items []string) string { + prompt := promptui.Select{ + Label: promptLabel, + Items: items, + } + + _, result, err := prompt.Run() + cobra.CheckErr(err) + + fmt.Printf("You selected: %s\n", result) + + return result +} + +// Check if Algod is installed +func isAlgodInstalled() bool { + if runtime.GOOS == "windows" { + panic("Windows is not supported.") + } + + return checkCmdToolExists("algod") +} + +// Checks that a bash cli/cmd tool exists +func checkCmdToolExists(tool string) bool { + _, err := exec.LookPath(tool) + return err == nil +} + +// Find where algod is defined and print its version +func printAlgodInfo() { + algodPath, err := exec.LookPath("algod") + if err != nil { + fmt.Printf("Error finding algod: %v\n", err) + return + } + + // Get algod version + algodVersion, err := exec.Command("algod", "-v").Output() + if err != nil { + fmt.Printf("Error getting algod version: %v\n", err) + return + } + + fmt.Printf("Algod is located at: %s\n", algodPath) + fmt.Printf("algod -v\n") + fmt.Printf("%s\n", algodVersion) +} + +// TODO: consider replacing with a method that does more for the user +func affectALGORAND_DATA(path string) { + fmt.Println("Please execute the following in your terminal to set the environment variable:") + fmt.Println("") + fmt.Println("export ALGORAND_DATA=" + path) + 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 +} + +// Finds path(s) to a file in a directory and its subdirectories using parallel processing +func findPathToFile(startDir string, targetFileName string) []string { + var dirPaths []string + var mu sync.Mutex + var wg sync.WaitGroup + + fileChan := make(chan string) + + // Worker function to process files + worker := func() { + defer wg.Done() + for path := range fileChan { + info, err := os.Stat(path) + if err != nil { + continue + } + if !info.IsDir() && info.Name() == targetFileName { + dirPath := filepath.Dir(path) + mu.Lock() + dirPaths = append(dirPaths, dirPath) + mu.Unlock() + } + } + } + + // Start worker goroutines + numWorkers := 4 // Adjust the number of workers based on your system's capabilities + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go worker() + } + + // Walk the directory tree and send file paths to the channel + err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + // Ignore permission errors + if os.IsPermission(err) { + return nil + } + return err + } + fileChan <- path + return nil + }) + + close(fileChan) + wg.Wait() + + if err != nil { + panic(err) + } + + return dirPaths +} + +func validateAlgorandDataDir(path string) bool { + info, err := os.Stat(path) + + // Check if the path exists + if os.IsNotExist(err) { + return false + } + + // Check if the path is a directory + if !info.IsDir() { + return false + } + + paths := findPathToFile(path, "algod.token") + if len(paths) == 1 { + return true + } + return false +} + +// Does a lazy check for Algorand data directories, based off of known common paths +func lazyCheckAlgorandDataDirs() []string { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Hardcoded paths known to be common Algorand data directories + commonAlgorandDataDirPaths := []string{ + "/var/lib/algorand", + filepath.Join(home, "node", "data"), + filepath.Join(home, ".algorand"), + } + + var paths []string + + for _, path := range commonAlgorandDataDirPaths { + if validateAlgorandDataDir(path) { + paths = append(paths, path) + } + } + + return paths +} + +// Checks if Algorand data directories exist, based off of existence of the "algod.token" file +func deepSearchAlgorandDataDirs() []string { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // TODO: consider a better way to identify an Algorand data directory + paths := findPathToFile(home, "algod.token") + + return paths +} + +func findAlgodPID() (int, error) { + cmd := exec.Command("pgrep", "algod") + output, err := cmd.Output() + if err != nil { + return 0, err + } + + var pid int + _, err = fmt.Sscanf(string(output), "%d", &pid) + if err != nil { + return 0, fmt.Errorf("failed to parse PID: %v", err) + } + + return pid, nil +} + +// Check systemctl has Algorand Service been created in the first place +func checkSystemctlAlgorandServiceCreated() bool { + cmd := exec.Command("systemctl", "list-unit-files", "algorand.service") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return false + } + return strings.Contains(out.String(), "algorand.service") +} + +func checkSystemctlAlgorandServiceActive() bool { + cmd := exec.Command("systemctl", "is-active", "algorand") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return false + } + return strings.TrimSpace(out.String()) == "active" +} + +// Extract version information from apt-cache policy output +func extractVersion(output, prefix string) string { + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, prefix) { + return strings.TrimSpace(strings.TrimPrefix(line, prefix)) + } + } + return "" +} diff --git a/go.mod b/go.mod index ebabe667..7bac0b36 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/charmbracelet/lipgloss v0.13.1 github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691 + github.com/manifoldco/promptui v0.9.0 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/cobra v1.8.1 @@ -19,6 +20,7 @@ require ( ) require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index acaa1e37..0b57cc4a 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,12 @@ github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691 h1:xiY github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691/go.mod h1:ektxP4TiEONm1mTGILRfo8F0a4rZMwsT1fEkXslQKtU= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -68,6 +74,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -137,6 +145,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/metrics.go b/internal/metrics.go index d2403094..b10a9f40 100644 --- a/internal/metrics.go +++ b/internal/metrics.go @@ -17,6 +17,9 @@ type MetricsModel struct { TPS float64 RX int TX int + LastTS time.Time + LastRX int + LastTX int } type MetricsResponse map[string]int diff --git a/internal/state.go b/internal/state.go index c20508c0..e14c07aa 100644 --- a/internal/state.go +++ b/internal/state.go @@ -91,8 +91,15 @@ func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client *api.Clien } if err == nil { s.Metrics.Enabled = true - s.Metrics.TX = res["algod_network_sent_bytes_total"] - s.Metrics.RX = res["algod_network_received_bytes_total"] + now := time.Now() + diff := now.Sub(s.Metrics.LastTS) + + s.Metrics.TX = max(0, int(float64(res["algod_network_sent_bytes_total"]-s.Metrics.LastTX)/diff.Seconds())) + s.Metrics.RX = max(0, int(float64(res["algod_network_received_bytes_total"]-s.Metrics.LastRX)/diff.Seconds())) + + s.Metrics.LastTS = now + s.Metrics.LastTX = res["algod_network_sent_bytes_total"] + s.Metrics.LastRX = res["algod_network_received_bytes_total"] } } func (s *StateModel) UpdateAccounts(client *api.ClientWithResponses) { diff --git a/ui/status.go b/ui/status.go index 281288bf..451a5758 100644 --- a/ui/status.go +++ b/ui/status.go @@ -6,6 +6,7 @@ import ( "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "math" "strconv" "strings" "time" @@ -44,6 +45,21 @@ func (m StatusViewModel) HandleMessage(msg tea.Msg) (StatusViewModel, tea.Cmd) { return m, nil } +func getBitRate(bytes int) string { + txString := fmt.Sprintf("%d B/s ", bytes) + if bytes >= 1024 { + txString = fmt.Sprintf("%d KB/s ", bytes/(1<<10)) + } + if bytes >= int(math.Pow(1024, 2)) { + txString = fmt.Sprintf("%d MB/s ", bytes/(1<<20)) + } + if bytes >= int(math.Pow(1024, 3)) { + txString = fmt.Sprintf("%d GB/s ", bytes/(1<<30)) + } + + return txString +} + // View handles the render cycle func (m StatusViewModel) View() string { if !m.IsVisible { @@ -70,13 +86,13 @@ func (m StatusViewModel) View() string { 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 = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + style.Green.Render("TX ") + end = getBitRate(m.Data.Metrics.TX) + 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 = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + style.Green.Render("RX ") + end = getBitRate(m.Data.Metrics.RX) + 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) 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 02/23] # 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 := `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <dict> + <key>Label</key> + <string>com.algorand.algod</string> + <key>ProgramArguments</key> + <array> + <string>{{.AlgodPath}}</string> + <string>-d</string> + <string>{{.DataDirectoryPath}}</string> + </array> + <key>RunAtLoad</key> + <true/> + <key>KeepAlive</key> + <true/> + <key>StandardOutPath</key> + <string>/tmp/algod.out</string> + <key>StandardErrorPath</key> + <string>/tmp/algod.err</string> + </dict> + </plist>` + + // 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(`<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>com.algorand.algod</string> + <key>ProgramArguments</key> + <array> + <string>%s/bin/algod</string> + <string>-d</string> + <string>%s/.algorand</string> + </array> + <key>RunAtLoad</key> + <true/> + <key>KeepAlive</key> + <true/> + <key>StandardOutPath</key> + <string>/tmp/algod.out</string> + <key>StandardErrorPath</key> + <string>/tmp/algod.err</string> +</dict> +</plist>`, 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 From 70456da3042c817791b37a5c69b43fe5b6a1dc7e Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:01:02 +0100 Subject: [PATCH 03/23] feat: add prompt-ui feat: configure and set algod data directory fix: RX/TX display fix: bit rate display for GB fix: configuration override order feat: handle invalid configuration and token gracefully test: fix test state fix: loading of custom endpoint address fix: loading default port test: clear viper settings fix: finds path to directory and gives cmd instruction feat: adds node start and node stop commands fix: add -y fix: turn script into indivudal commands feat: check if sudo, clarify shell chore: make more go idiomatic fix: fix proper path check fix: interact with systemctl, cleanup prompts fix: remove sudo fix: separate commands fix: proper algorand service name fix: calling with sudo chore: testing systemctl fix: checks algorand system service has been enabled directly feat: implements editAlgorandServiceFile fix: else statement fix: quick check branch fix: string template feat: adds upgrade chore: removeu nnecessary code fix: check that installed and candidate are the same chore: improve print chore: add more output fix: single quote fix: -y fix: systemctl fix: upgrade and sudo text chore: go mod tidy fix: upgrade feat: disable ui elements while syncing feat: skip account loading on syncing feat: remove offline account expires date feat: installs algod and sets up service on mac feat: refactor, + mac feat: adds uninstall, mac only fix: remove plist file chore: rename test: protocol snapshots and 100% --- ui/protocol_test.go | 120 +++++++++++++++++- .../Test_ProtocolSnapshot/Hidden.golden | 0 .../Test_ProtocolSnapshot/HiddenHeight.golden | 0 .../NoVoteOrUpgrade.golden | 7 + .../NoVoteOrUpgradeSmall.golden | 7 + .../Test_ProtocolSnapshot/Visible.golden | 7 + .../Test_ProtocolSnapshot/VisibleSmall.golden | 7 + 7 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 ui/testdata/Test_ProtocolSnapshot/Hidden.golden create mode 100644 ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden create mode 100644 ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden create mode 100644 ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden create mode 100644 ui/testdata/Test_ProtocolSnapshot/Visible.golden create mode 100644 ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden diff --git a/ui/protocol_test.go b/ui/protocol_test.go index 74e1d672..99c3e63b 100644 --- a/ui/protocol_test.go +++ b/ui/protocol_test.go @@ -2,14 +2,108 @@ package ui import ( "bytes" + "testing" + "time" + "github.com/algorandfoundation/hack-tui/internal" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" - "testing" - "time" ) -func Test_ProtocolViewRender(t *testing.T) { +var protocolViewSnapshots = map[string]ProtocolViewModel{ + "Hidden": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: true, + NeedsUpdate: true, + LastRound: 0, + }, + TerminalWidth: 60, + TerminalHeight: 40, + IsVisible: false, + }, + "HiddenHeight": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: true, + NeedsUpdate: true, + LastRound: 0, + }, + TerminalWidth: 70, + TerminalHeight: 20, + IsVisible: true, + }, + "Visible": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: true, + NeedsUpdate: true, + LastRound: 0, + }, + TerminalWidth: 160, + TerminalHeight: 80, + IsVisible: true, + }, + "VisibleSmall": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: true, + NeedsUpdate: true, + LastRound: 0, + }, + TerminalWidth: 80, + TerminalHeight: 40, + IsVisible: true, + }, + "NoVoteOrUpgrade": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: false, + NeedsUpdate: false, + LastRound: 0, + }, + TerminalWidth: 160, + TerminalHeight: 80, + IsVisible: true, + }, + "NoVoteOrUpgradeSmall": { + Data: internal.StatusModel{ + State: "SYNCING", + Version: "v0.0.0-test", + Network: "test-v1", + Voting: false, + NeedsUpdate: false, + LastRound: 0, + }, + TerminalWidth: 80, + TerminalHeight: 40, + IsVisible: true, + }, +} + +func Test_ProtocolSnapshot(t *testing.T) { + for name, model := range protocolViewSnapshots { + t.Run(name, func(t *testing.T) { + got := ansi.Strip(model.View()) + golden.RequireEqual(t, []byte(got)) + }) + } +} + +// Test_ProtocolMessages handles any additional tests like sending messages +func Test_ProtocolMessages(t *testing.T) { state := internal.StateModel{ Status: internal.StatusModel{ LastRound: 1337, @@ -41,9 +135,25 @@ func Test_ProtocolViewRender(t *testing.T) { teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3), ) + tm.Send(internal.StatusModel{ + State: "", + Version: "", + Network: "", + Voting: false, + NeedsUpdate: false, + LastRound: 0, + }) + // Send hide key + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("h"), + }) - // Send quit msg - tm.Send(tea.QuitMsg{}) + // Send quit key + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("ctrl+c"), + }) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/testdata/Test_ProtocolSnapshot/Hidden.golden b/ui/testdata/Test_ProtocolSnapshot/Hidden.golden new file mode 100644 index 00000000..e69de29b diff --git a/ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden b/ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden new file mode 100644 index 00000000..e69de29b diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden new file mode 100644 index 00000000..6eca90f5 --- /dev/null +++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden @@ -0,0 +1,7 @@ +╭──────────────────────────────────────────────────────────────────────────────╮ +│ Node: v0.0.0-test │ +│ │ +│ Network: test-v1 │ +│ │ +│ Protocol Voting: false │ +╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden new file mode 100644 index 00000000..a0cc43b9 --- /dev/null +++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden @@ -0,0 +1,7 @@ +╭──────────────────────────────────────────────────────────────────────────────╮ +│ Node: v0.0.0-test │ +│ Network: test-v1 │ +│ Protocol Voting: false │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/ui/testdata/Test_ProtocolSnapshot/Visible.golden b/ui/testdata/Test_ProtocolSnapshot/Visible.golden new file mode 100644 index 00000000..a8adee3f --- /dev/null +++ b/ui/testdata/Test_ProtocolSnapshot/Visible.golden @@ -0,0 +1,7 @@ +╭──────────────────────────────────────────────────────────────────────────────╮ +│ Node: v0.0.0-test [UPDATE AVAILABLE] │ +│ │ +│ Network: test-v1 │ +│ │ +│ Protocol Voting: true │ +╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden b/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden new file mode 100644 index 00000000..10a5f701 --- /dev/null +++ b/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden @@ -0,0 +1,7 @@ +╭──────────────────────────────────────────────────────────────────────────────╮ +│ Node: v0.0.0-test │ +│ Network: test-v1 │ +│ Protocol Voting: true │ +│ Upgrade Available: true │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file From ecf00bd8ac945646fc3d5d0d8e615ddd9ba0c237 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:01:59 +0100 Subject: [PATCH 04/23] # This is a combination of 51 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 # This is the commit message #47: test: protocol snapshots and 100% # This is the commit message #48: test: status snapshots and 100% # This is the commit message #49: test: error page snapshots and 100% # This is the commit message #50: test: controls snapshots # This is the commit message #51: test: accounts snapshots --- ui/controls/controls_test.go | 12 +- .../testdata/Test_Snapshot/Visible.golden | 3 + ui/error_test.go | 52 +++++++++ ui/pages/accounts/accounts_test.go | 106 ++++++++++++++++++ .../testdata/Test_Snapshot/Visible.golden | 26 +++++ ui/status_test.go | 82 +++++++++++++- ui/testdata/Test_ErrorSnapshot/Visible.golden | 24 ++++ ui/testdata/Test_StatusSnapshot/Hidden.golden | 0 .../Test_StatusSnapshot/Loading.golden | 6 + .../Test_StatusSnapshot/Syncing.golden | 7 ++ 10 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 ui/controls/testdata/Test_Snapshot/Visible.golden create mode 100644 ui/error_test.go create mode 100644 ui/pages/accounts/accounts_test.go create mode 100644 ui/pages/accounts/testdata/Test_Snapshot/Visible.golden create mode 100644 ui/testdata/Test_ErrorSnapshot/Visible.golden create mode 100644 ui/testdata/Test_StatusSnapshot/Hidden.golden create mode 100644 ui/testdata/Test_StatusSnapshot/Loading.golden create mode 100644 ui/testdata/Test_StatusSnapshot/Syncing.golden diff --git a/ui/controls/controls_test.go b/ui/controls/controls_test.go index ed1811a1..2693afa3 100644 --- a/ui/controls/controls_test.go +++ b/ui/controls/controls_test.go @@ -3,12 +3,22 @@ package controls import ( "bytes" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" "testing" "time" ) -func Test_Controls(t *testing.T) { +func Test_Snapshot(t *testing.T) { + t.Run("Visible", func(t *testing.T) { + model := New(" test ") + got := ansi.Strip(model.View()) + golden.RequireEqual(t, []byte(got)) + }) +} + +func Test_Messages(t *testing.T) { expected := "(q)uit | (d)elete | (g)enerate | (t)xn | (h)ide" // Create the Model m := New(expected) diff --git a/ui/controls/testdata/Test_Snapshot/Visible.golden b/ui/controls/testdata/Test_Snapshot/Visible.golden new file mode 100644 index 00000000..8f20fb4b --- /dev/null +++ b/ui/controls/testdata/Test_Snapshot/Visible.golden @@ -0,0 +1,3 @@ + ╭──────╮ +────────────────────────────────────┤ test ├──────────────────────────────────── + ╰──────╯ \ No newline at end of file diff --git a/ui/error_test.go b/ui/error_test.go new file mode 100644 index 00000000..10020eac --- /dev/null +++ b/ui/error_test.go @@ -0,0 +1,52 @@ +package ui + +import ( + "bytes" + "github.com/algorandfoundation/hack-tui/ui/controls" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" + "github.com/charmbracelet/x/exp/teatest" + "testing" + "time" +) + +func Test_ErrorSnapshot(t *testing.T) { + t.Run("Visible", func(t *testing.T) { + model := ErrorViewModel{ + Height: 20, + Width: 40, + controls: controls.New(" Error "), + Message: "a test error", + } + got := ansi.Strip(model.View()) + golden.RequireEqual(t, []byte(got)) + }) +} + +func Test_ErrorMessages(t *testing.T) { + tm := teatest.NewTestModel( + t, ErrorViewModel{Message: "a test error"}, + teatest.WithInitialTermSize(120, 80), + ) + + // Wait for prompt to exit + teatest.WaitFor( + t, tm.Output(), + func(bts []byte) bool { + return bytes.Contains(bts, []byte("a test error")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + // Resize Message + tm.Send(tea.WindowSizeMsg{ + Width: 50, + Height: 20, + }) + + // Send quit key + tm.Send(tea.QuitMsg{}) + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} diff --git a/ui/pages/accounts/accounts_test.go b/ui/pages/accounts/accounts_test.go new file mode 100644 index 00000000..d6afa218 --- /dev/null +++ b/ui/pages/accounts/accounts_test.go @@ -0,0 +1,106 @@ +package accounts + +import ( + "bytes" + "github.com/algorandfoundation/hack-tui/api" + "github.com/algorandfoundation/hack-tui/internal" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" + "github.com/charmbracelet/x/exp/teatest" + "testing" + "time" +) + +func Test_Snapshot(t *testing.T) { + t.Run("Visible", func(t *testing.T) { + model := New(&internal.StateModel{ + Status: internal.StatusModel{}, + Metrics: internal.MetricsModel{}, + Accounts: nil, + ParticipationKeys: nil, + Admin: false, + Watching: false, + }) + got := ansi.Strip(model.View()) + golden.RequireEqual(t, []byte(got)) + }) +} + +func Test_Messages(t *testing.T) { + var testKeys = []api.ParticipationKey{ + { + Address: "ABC", + EffectiveFirstValid: nil, + EffectiveLastValid: nil, + Id: "", + Key: api.AccountParticipation{ + SelectionParticipationKey: nil, + StateProofKey: nil, + VoteFirstValid: 0, + VoteKeyDilution: 0, + VoteLastValid: 0, + VoteParticipationKey: nil, + }, + LastBlockProposal: nil, + LastStateProof: nil, + LastVote: nil, + }, + } + sm := &internal.StateModel{ + Status: internal.StatusModel{}, + Metrics: internal.MetricsModel{}, + Accounts: nil, + ParticipationKeys: &testKeys, + Admin: false, + Watching: false, + } + values := make(map[string]internal.Account) + for _, key := range *sm.ParticipationKeys { + val, ok := values[key.Address] + if !ok { + values[key.Address] = internal.Account{ + Address: key.Address, + Status: "Offline", + Balance: 0, + Expires: time.Unix(0, 0), + Keys: 1, + } + } else { + val.Keys++ + values[key.Address] = val + } + } + sm.Accounts = values + // Create the Model + m := New(sm) + + tm := teatest.NewTestModel( + t, m, + teatest.WithInitialTermSize(80, 40), + ) + + // Wait for prompt to exit + teatest.WaitFor( + t, tm.Output(), + func(bts []byte) bool { + return bytes.Contains(bts, []byte("(k)eys")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(*sm) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("enter"), + }) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("ctrl+c"), + }) + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} diff --git a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden new file mode 100644 index 00000000..0c6418cd --- /dev/null +++ b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + ╭──────────────────────────────────────────╮ +──────────────────┤ (g)enerate | (a)ccounts | (k)eys | (t)xn ├────────────────── + ╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/ui/status_test.go b/ui/status_test.go index 83e77ab8..823a0202 100644 --- a/ui/status_test.go +++ b/ui/status_test.go @@ -3,6 +3,10 @@ package ui import ( "bytes" "github.com/algorandfoundation/hack-tui/internal" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" + "github.com/charmbracelet/x/exp/teatest" "testing" "time" @@ -10,8 +14,67 @@ import ( "github.com/charmbracelet/x/exp/teatest" ) -func Test_StatusViewRender(t *testing.T) { +var statusViewSnapshots = map[string]StatusViewModel{ + "Syncing": { + Data: &internal.StateModel{ + Status: internal.StatusModel{ + LastRound: 1337, + NeedsUpdate: true, + State: "SYNCING", + }, + Metrics: internal.MetricsModel{ + RoundTime: 0, + TX: 0, + }, + }, + TerminalWidth: 180, + TerminalHeight: 80, + IsVisible: true, + }, + "Hidden": { + Data: &internal.StateModel{ + Status: internal.StatusModel{ + LastRound: 1337, + NeedsUpdate: true, + State: "SYNCING", + }, + Metrics: internal.MetricsModel{ + RoundTime: 0, + TX: 0, + }, + }, + TerminalWidth: 180, + TerminalHeight: 80, + IsVisible: false, + }, + "Loading": { + Data: &internal.StateModel{ + Status: internal.StatusModel{ + LastRound: 1337, + NeedsUpdate: true, + State: "SYNCING", + }, + Metrics: internal.MetricsModel{ + RoundTime: 0, + TX: 0, + }, + }, + TerminalWidth: 0, + TerminalHeight: 0, + IsVisible: true, + }, +} +func Test_StatusSnapshot(t *testing.T) { + for name, model := range statusViewSnapshots { + t.Run(name, func(t *testing.T) { + got := ansi.Strip(model.View()) + golden.RequireEqual(t, []byte(got)) + }) + } +} + +func Test_StatusMessages(t *testing.T) { state := internal.StateModel{ Status: internal.StatusModel{ LastRound: 1337, @@ -47,8 +110,25 @@ func Test_StatusViewRender(t *testing.T) { teatest.WithDuration(time.Second*3), ) +<<<<<<< HEAD // Send quit msg tm.Send(tea.QuitMsg{}) +======= + // Send the state + tm.Send(state) + + // Send hide key + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("h"), + }) + + // Send quit key + tm.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("ctrl+c"), + }) +>>>>>>> 967fa6b (test: status snapshots and 100%) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/testdata/Test_ErrorSnapshot/Visible.golden b/ui/testdata/Test_ErrorSnapshot/Visible.golden new file mode 100644 index 00000000..80bcfb88 --- /dev/null +++ b/ui/testdata/Test_ErrorSnapshot/Visible.golden @@ -0,0 +1,24 @@ + + + + + + + + + + + a test error + + + + + + + + + + + ╭───────╮ +───────────────────────────────────┤ Error ├─────────────────────────────────── + ╰───────╯ \ No newline at end of file diff --git a/ui/testdata/Test_StatusSnapshot/Hidden.golden b/ui/testdata/Test_StatusSnapshot/Hidden.golden new file mode 100644 index 00000000..e69de29b diff --git a/ui/testdata/Test_StatusSnapshot/Loading.golden b/ui/testdata/Test_StatusSnapshot/Loading.golden new file mode 100644 index 00000000..fe45233a --- /dev/null +++ b/ui/testdata/Test_StatusSnapshot/Loading.golden @@ -0,0 +1,6 @@ +Loading... + + + + + diff --git a/ui/testdata/Test_StatusSnapshot/Syncing.golden b/ui/testdata/Test_StatusSnapshot/Syncing.golden new file mode 100644 index 00000000..7edf0f7e --- /dev/null +++ b/ui/testdata/Test_StatusSnapshot/Syncing.golden @@ -0,0 +1,7 @@ +╭────────────────────────────────────────────────────────────────────────────────────────╮ +│ Latest Round: 1337 SYNCING │ +│ │ +│ -- 0 round average -- │ +│ Round time: 0.00s 0 KB/s TX │ +│ TPS: 0.00 0 KB/s RX │ +╰────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file From 415ea4628ff867f9dd7a8998a2b3a49f83779bd9 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:02:20 +0100 Subject: [PATCH 05/23] feat: add prompt-ui feat: configure and set algod data directory fix: RX/TX display fix: bit rate display for GB fix: configuration override order feat: handle invalid configuration and token gracefully test: fix test state fix: loading of custom endpoint address fix: loading default port test: clear viper settings fix: finds path to directory and gives cmd instruction feat: adds node start and node stop commands fix: add -y fix: turn script into indivudal commands feat: check if sudo, clarify shell chore: make more go idiomatic fix: fix proper path check fix: interact with systemctl, cleanup prompts fix: remove sudo fix: separate commands fix: proper algorand service name fix: calling with sudo chore: testing systemctl fix: checks algorand system service has been enabled directly feat: implements editAlgorandServiceFile fix: else statement fix: quick check branch fix: string template feat: adds upgrade chore: removeu nnecessary code fix: check that installed and candidate are the same chore: improve print chore: add more output fix: single quote fix: -y fix: systemctl fix: upgrade and sudo text chore: go mod tidy fix: upgrade feat: disable ui elements while syncing feat: skip account loading on syncing feat: remove offline account expires date feat: installs algod and sets up service on mac feat: refactor, + mac feat: adds uninstall, mac only fix: remove plist file chore: rename test: protocol snapshots and 100% test: status snapshots and 100% test: error page snapshots and 100% test: controls snapshots test: accounts snapshots build: go mod tidy README: Add alpha/dev warnings & notes README: Moved note README: added note README: added note about admin token README: reordered README: numbered build steps README: final note test: add codecov reporter ci: temp node test flow chore: only when push systemctl ENABLE fix: rm -f file fix: install start and uninstall for redhat fix: add systemctl check chore: printout fix: ubuntu-based uninstall fix: uninstallation remove service fix: adds extar systemctl daemon-reload chroe: temp fix: provide output chore: dont fail silently fix: wildcard fix: remove extra daemon-reload fix: remore unnecessary output chore: rephrase ci: run in pushes --- .github/workflows/code_test.yaml | 8 ++-- .github/workflows/node_test.yaml | 58 +++++++++++++++++++++++++++ .testcoverage.yaml | 26 ------------- README.md | 33 ++++++++++++---- cmd/node/install.go | 26 ++++++------- cmd/node/uninstall.go | 67 ++++++++++++++++++++++++++++++-- go.mod | 4 +- 7 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/node_test.yaml delete mode 100644 .testcoverage.yaml diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml index 2ab07538..cd6a095e 100644 --- a/.github/workflows/code_test.yaml +++ b/.github/workflows/code_test.yaml @@ -56,9 +56,9 @@ jobs: run: go build -o bin/algorun *.go - name: Test with the Go CLI - run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... + run: go test ./... -coverprofile=./coverage.txt -covermode=atomic -coverpkg=./... - - name: Check test coverage - uses: vladopajic/go-test-coverage@v2 + - name: Upload results to Codecov + uses: codecov/codecov-action@v4 with: - config: ./.testcoverage.yaml + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml new file mode 100644 index 00000000..4e62f691 --- /dev/null +++ b/.github/workflows/node_test.yaml @@ -0,0 +1,58 @@ +## This is a temporary flow, until we have our custom docker images that work with systemd for linux. +## Once we have that, we can remove this and use docker containers in parallel, covering the various OS:es. + +name: Node Command OS-Matrix Test + +on: + workflow_dispatch: + push: + paths: + - "cmd/**" + pull_request: + paths: + - "cmd/**" + +jobs: + ubuntu: + runs-on: ubuntu-latest + + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: 1.22 + - name: Run Ubuntu commands + run: | + git clone https://github.com/algorandfoundation/hack-tui.git + cd hack-tui + go build . + sudo ./hack-tui node install + sudo ./hack-tui node start + systemctl status algorand.service + export TOKEN=$(cat /var/lib/algorand/algod.admin.token) + curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" + sudo ./hack-tui node stop + sudo ./hack-tui node upgrade + # sudo ./hack-tui node configure + sudo ./hack-tui node uninstall + + macos: + runs-on: macos-latest + steps: + - name: Run MacOs commands + run: | + brew install go + cd ~/ + git clone https://github.com/algorandfoundation/hack-tui.git + cd hack-tui + go build . + sudo ./hack-tui node install + sudo ./hack-tui node start + sudo launchctl print system/com.algorand.algod + sleep 5 + export TOKEN=$(cat ~/.algorand/algod.admin.token) + curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" + sudo ./hack-tui node stop + sudo ./hack-tui node upgrade + # sudo ./hack-tui node configure + sudo ./hack-tui node uninstall diff --git a/.testcoverage.yaml b/.testcoverage.yaml deleted file mode 100644 index 18363129..00000000 --- a/.testcoverage.yaml +++ /dev/null @@ -1,26 +0,0 @@ -profile: cover.out - -# (optional; but recommended to set) -# When specified reported file paths will not contain local prefix in the output -local-prefix: "github.com/algorandfoundation/hack-tui" - -# Holds coverage thresholds percentages, values should be in range [0-100] -threshold: - # (optional; default 0) - # The minimum coverage that each file should have - file: 0 - - # (optional; default 0) - # The minimum coverage that each package should have - package: 0 - - # (optional; default 0) - # The minimum total coverage project should have - total: 0 - -# Holds regexp rules which will exclude matched files or packages -# from coverage statistics -exclude: - # Exclude files or packages matching their paths - paths: - - api/if.go diff --git a/README.md b/README.md index 4e0418b1..d419c150 100644 --- a/README.md +++ b/README.md @@ -20,44 +20,60 @@ --- Terminal UI for managing Algorand nodes. -Built with [bubbles](https://github.com/charmbracelet/bubbles)/[bubbletea](https://github.com/charmbracelet/bubbletea) +Built with [bubbles](https://github.com/charmbracelet/bubbles) & [bubbletea](https://github.com/charmbracelet/bubbletea) + +> [!CAUTION] +> This project is in alpha state and under heavy development. We do not recommend performing actions (e.g. key management) on participation nodes connected to public networks. # 🚀 Get Started Run the build or ~~download the latest cli(WIP)~~. +> [!NOTE] +> We do not have pre-built binaries yet. If you are comfortable doing so, you are welcome to build it yourself and provide feedback. + ## Building -Clone the repository +1. Clone the repository ```bash -git clone git@github.com:algorandfoundation/hack-tui.git +git clone https://github.com/algorandfoundation/hack-tui.git ``` -Change to the project directory +2. Change to the project directory ```bash cd hack-tui ``` -Run the build command +3. Run the build command ```bash make build ``` -Start a participation node +4. Start a participation node ```bash docker compose up ``` -Connect to the node +> [!NOTE] +> The docker image is used for development and testing purposes. TUI will also work with native algod. +> If you have a node installed already, you can skip this step. + +5. Connect to the node ```bash ./bin/algorun --server http://localhost:8080 --token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` +> [!CAUTION] +> This project is in alpha state and under heavy development. We do not recommend performing actions (e.g. key management) on participation nodes connected to public networks. + +> [!NOTE] +> If you skipped the docker container, try running `./bin/algorun` standalone, which will detect your algorand data directory from the `ALGORAND_DATA` environment variable that works for `goal`. Otherwise, provide the `--server` and `--token` arguments so that it can find your node. Note that algorun requires the admin algod token. + # ℹ️ Usage ## ⚙️ Configuration @@ -99,6 +115,9 @@ The application supports the `server` and `token` flags for configuration. algorun --server http://localhost:8080 --token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` +> [!IMPORTANT] +> TUI requires the *admin* token in order to access participation key information. This can be found in the `algod.admin.token` file, e.g. `/var/lib/algorand/algod.admin.token` + ## 🧑💻 Commands The default command will launch the full TUI application diff --git a/cmd/node/install.go b/cmd/node/install.go index e714e710..c0f08fee 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -25,6 +25,12 @@ var installCmd = &cobra.Command{ func installNode() { fmt.Println("Checking if Algod is installed...") + // 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 Algod is installed if !isAlgodInstalled() { fmt.Println("Algod is not installed. Installing...") @@ -48,12 +54,6 @@ func installNode() { 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 @@ -87,7 +87,9 @@ func installNodeLinux() { {"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"}, + {"systemctl", "enable", "algorand.service"}, + {"systemctl", "start", "algorand.service"}, + {"rm", "-f", "rpm_algorand.pub"}, } } else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager fmt.Println("Using yum package manager") @@ -97,7 +99,9 @@ func installNodeLinux() { {"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"}, + {"systemctl", "enable", "algorand.service"}, + {"systemctl", "start", "algorand.service"}, + {"rm", "-f", "rpm_algorand.pub"}, } } 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.") @@ -134,12 +138,6 @@ func installNodeLinux() { 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.") diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go index e20d3c74..99834409 100644 --- a/cmd/node/uninstall.go +++ b/cmd/node/uninstall.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "runtime" + "strings" "github.com/spf13/cobra" ) @@ -34,14 +35,14 @@ func unInstallNode() { 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*.") + fmt.Println("Algod is running. Please run *node stop* first to stop it.") os.Exit(1) } + fmt.Println("Algod is installed. Proceeding...") + // Uninstall Algod based on OS switch runtime.GOOS { case "linux": @@ -96,4 +97,62 @@ func unInstallNodeMac() { fmt.Println("Algorand uninstalled successfully.") } -func unInstallNodeLinux() {} +func unInstallNodeLinux() { + + var unInstallCmds [][]string + + if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager + fmt.Println("Using apt package manager") + unInstallCmds = [][]string{ + {"apt", "remove", "algorand-devtools", "-y"}, + {"apt", "autoremove", "-y"}, + } + } else if checkCmdToolExists("apt-get") { + fmt.Println("Using apt-get package manager") + unInstallCmds = [][]string{ + {"apt-get", "remove", "algorand-devtools", "-y"}, + {"apt-get", "autoremove", "-y"}, + } + } else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager + fmt.Println("Using dnf package manager") + unInstallCmds = [][]string{ + {"dnf", "remove", "algorand-devtools", "-y"}, + } + } else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager + fmt.Println("Using yum package manager") + unInstallCmds = [][]string{ + {"yum", "remove", "algorand-devtools", "-y"}, + } + } else { + fmt.Println("Could not find a package manager to uninstall Algorand.") + os.Exit(1) + } + + // Commands to clear systemd algorand.service and any other files, like the configuration override + unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "sudo rm -rf /etc/systemd/system/algorand*"}) + unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"}) + + // Run each installation command + for _, cmdArgs := range unInstallCmds { + 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) + } + } + + // Check the status of the algorand service + cmd := exec.Command("systemctl", "status", "algorand") + output, err := cmd.CombinedOutput() + if err != nil && strings.Contains(string(output), "Unit algorand.service could not be found.") { + fmt.Println("Algorand service has been successfully removed.") + } else { + fmt.Printf("Failed to verify Algorand service uninstallation: %v\n", err) + fmt.Printf("Output: %s\n", string(output)) + os.Exit(1) + } + + fmt.Println("Algorand successfully uninstalled.") +} diff --git a/go.mod b/go.mod index 7bac0b36..217ad5af 100644 --- a/go.mod +++ b/go.mod @@ -34,8 +34,8 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.3.2 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20241022174419-46d9bb99a691 // indirect + github.com/charmbracelet/x/ansi v0.3.2 + github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b github.com/charmbracelet/x/term v0.2.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect From 0ffecc52c6b81105c27c2df2467b5baadc581c29 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:13:40 +0100 Subject: [PATCH 06/23] chore: fix symbols chore: return msg.quit chore: fix imports chore: adjust imports chore: space chore: fix space chore: fix status chore: fix merge error chore: return docs --- cmd/status_test.go | 3 ++- internal/accounts_test.go | 5 ++-- internal/block.go | 3 ++- internal/block_test.go | 3 ++- internal/metrics.go | 3 ++- internal/state_test.go | 5 ++-- main_test.go | 3 ++- ui/README.md | 10 ++++--- ui/controls/controls_test.go | 5 ++-- ui/controls/view.go | 3 ++- ui/error.go | 3 ++- ui/error_test.go | 5 ++-- ui/pages/accounts/model.go | 3 ++- ui/pages/generate/controller.go | 3 ++- ui/pages/generate/style.go | 1 + ui/pages/generate/view.go | 3 ++- ui/pages/transaction/view.go | 3 ++- ui/protocol.go | 5 ++-- ui/protocol_test.go | 13 ++-------- ui/status_test.go | 26 +------------------ .../NoVoteOrUpgrade.golden | 4 --- .../NoVoteOrUpgradeSmall.golden | 4 --- .../Test_ProtocolSnapshot/Visible.golden | 4 --- 23 files changed, 47 insertions(+), 73 deletions(-) diff --git a/cmd/status_test.go b/cmd/status_test.go index 9d2a6087..5e694234 100644 --- a/cmd/status_test.go +++ b/cmd/status_test.go @@ -2,8 +2,9 @@ package cmd import ( "context" - "github.com/spf13/viper" "testing" + + "github.com/spf13/viper" ) func Test_ExecuteInvalidStatusCommand(t *testing.T) { diff --git a/internal/accounts_test.go b/internal/accounts_test.go index 234bf7f8..04648169 100644 --- a/internal/accounts_test.go +++ b/internal/accounts_test.go @@ -1,11 +1,12 @@ package internal import ( + "testing" + "time" + "github.com/algorandfoundation/hack-tui/api" "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" "github.com/stretchr/testify/assert" - "testing" - "time" ) type TestClock struct{} diff --git a/internal/block.go b/internal/block.go index 5af2fbae..825932bc 100644 --- a/internal/block.go +++ b/internal/block.go @@ -3,8 +3,9 @@ package internal import ( "context" "errors" - "github.com/algorandfoundation/hack-tui/api" "time" + + "github.com/algorandfoundation/hack-tui/api" ) type BlockMetrics struct { diff --git a/internal/block_test.go b/internal/block_test.go index 16156eed..d886db26 100644 --- a/internal/block_test.go +++ b/internal/block_test.go @@ -2,9 +2,10 @@ package internal import ( "context" - "github.com/algorandfoundation/hack-tui/api" "testing" "time" + + "github.com/algorandfoundation/hack-tui/api" ) func Test_GetBlockMetrics(t *testing.T) { diff --git a/internal/metrics.go b/internal/metrics.go index b10a9f40..6f4e09bc 100644 --- a/internal/metrics.go +++ b/internal/metrics.go @@ -3,11 +3,12 @@ package internal import ( "context" "errors" - "github.com/algorandfoundation/hack-tui/api" "regexp" "strconv" "strings" "time" + + "github.com/algorandfoundation/hack-tui/api" ) type MetricsModel struct { diff --git a/internal/state_test.go b/internal/state_test.go index 769c6639..2afeaf1c 100644 --- a/internal/state_test.go +++ b/internal/state_test.go @@ -2,10 +2,11 @@ package internal import ( "context" - "github.com/algorandfoundation/hack-tui/api" - "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" "testing" "time" + + "github.com/algorandfoundation/hack-tui/api" + "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" ) func Test_StateModel(t *testing.T) { diff --git a/main_test.go b/main_test.go index 40521d59..a7e99a2e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,8 +1,9 @@ package main import ( - "github.com/spf13/viper" "testing" + + "github.com/spf13/viper" ) func Test_Main(t *testing.T) { diff --git a/ui/README.md b/ui/README.md index a80ddf51..ed10387c 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,13 +1,13 @@ # Overview -The ui package contains bubbletea interfaces. +The ui package contains bubbletea interfaces. ## Common practices A `style.go` file holds lipgloss predefined styles for the package. All components are instances of a `tea.Model` which is composed of models -from the `internal` package. +from the `internal` package. Components can either be single file or independent packages. Example for `status.go` single file component: @@ -26,12 +26,13 @@ func (m StatusViewModel) Int(){} //other tea.Model interfaces ... ``` -Once the component is sufficiently complex or needs to be reused, it can be moved +Once the component is sufficiently complex or needs to be reused, it can be moved to its own package Example refactor for `status.go` to a package: #### ui/status/model.go + ```go package status import "github.com/algorandfoundation/hack-tui/internal" @@ -43,6 +44,7 @@ type ViewModel struct { ``` #### ui/status/controller.go + ```go package status @@ -84,4 +86,4 @@ package status import "github.com/charmbracelet/lipgloss" var someStyle = lipgloss.NewStyle() -``` \ No newline at end of file +``` diff --git a/ui/controls/controls_test.go b/ui/controls/controls_test.go index 2693afa3..ccef26c0 100644 --- a/ui/controls/controls_test.go +++ b/ui/controls/controls_test.go @@ -2,12 +2,13 @@ package controls import ( "bytes" + "testing" + "time" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" - "testing" - "time" ) func Test_Snapshot(t *testing.T) { diff --git a/ui/controls/view.go b/ui/controls/view.go index 3754f42c..c10265cf 100644 --- a/ui/controls/view.go +++ b/ui/controls/view.go @@ -1,8 +1,9 @@ package controls import ( - "github.com/charmbracelet/lipgloss" "strings" + + "github.com/charmbracelet/lipgloss" ) // View renders the model's content if it is visible, aligning it horizontally and ensuring it fits within the specified width. diff --git a/ui/error.go b/ui/error.go index c0678d7d..2346b896 100644 --- a/ui/error.go +++ b/ui/error.go @@ -1,11 +1,12 @@ package ui import ( + "strings" + "github.com/algorandfoundation/hack-tui/ui/controls" "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "strings" ) type ErrorViewModel struct { diff --git a/ui/error_test.go b/ui/error_test.go index 10020eac..0613c433 100644 --- a/ui/error_test.go +++ b/ui/error_test.go @@ -2,13 +2,14 @@ package ui import ( "bytes" + "testing" + "time" + "github.com/algorandfoundation/hack-tui/ui/controls" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" - "testing" - "time" ) func Test_ErrorSnapshot(t *testing.T) { diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go index 91b550cb..dabe1b46 100644 --- a/ui/pages/accounts/model.go +++ b/ui/pages/accounts/model.go @@ -1,11 +1,12 @@ package accounts import ( - "github.com/algorandfoundation/hack-tui/ui/style" "sort" "strconv" "time" + "github.com/algorandfoundation/hack-tui/ui/style" + "github.com/algorandfoundation/hack-tui/internal" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" diff --git a/ui/pages/generate/controller.go b/ui/pages/generate/controller.go index c8adbaa6..0ebe2346 100644 --- a/ui/pages/generate/controller.go +++ b/ui/pages/generate/controller.go @@ -2,6 +2,8 @@ package generate import ( "context" + "strconv" + "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui/pages/accounts" @@ -10,7 +12,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" - "strconv" ) func (m ViewModel) Init() tea.Cmd { diff --git a/ui/pages/generate/style.go b/ui/pages/generate/style.go index 8783d051..7d53d321 100644 --- a/ui/pages/generate/style.go +++ b/ui/pages/generate/style.go @@ -2,6 +2,7 @@ package generate import ( "fmt" + "github.com/charmbracelet/lipgloss" ) diff --git a/ui/pages/generate/view.go b/ui/pages/generate/view.go index 667410ea..b61f2c0f 100644 --- a/ui/pages/generate/view.go +++ b/ui/pages/generate/view.go @@ -2,8 +2,9 @@ package generate import ( "fmt" - "github.com/algorandfoundation/hack-tui/ui/style" "strings" + + "github.com/algorandfoundation/hack-tui/ui/style" ) func (m ViewModel) View() string { diff --git a/ui/pages/transaction/view.go b/ui/pages/transaction/view.go index 795841c1..029fa843 100644 --- a/ui/pages/transaction/view.go +++ b/ui/pages/transaction/view.go @@ -1,9 +1,10 @@ package transaction import ( + "strings" + "github.com/algorandfoundation/hack-tui/ui/style" "github.com/charmbracelet/lipgloss" - "strings" ) func (m ViewModel) View() string { diff --git a/ui/protocol.go b/ui/protocol.go index 7126e6b5..177abc18 100644 --- a/ui/protocol.go +++ b/ui/protocol.go @@ -1,12 +1,13 @@ package ui import ( + "strconv" + "strings" + "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "strconv" - "strings" ) // ProtocolViewModel includes the internal.StatusModel and internal.MetricsModel diff --git a/ui/protocol_test.go b/ui/protocol_test.go index 2b04df54..da60005c 100644 --- a/ui/protocol_test.go +++ b/ui/protocol_test.go @@ -10,12 +10,6 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" - - "github.com/algorandfoundation/hack-tui/internal" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/exp/golden" - "github.com/charmbracelet/x/exp/teatest" ) var protocolViewSnapshots = map[string]ProtocolViewModel{ @@ -161,11 +155,8 @@ func Test_ProtocolMessages(t *testing.T) { Runes: []rune("ctrl+c"), }) - // Send quit key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("ctrl+c"), - }) + // Send quit msg + tm.Send(tea.QuitMsg{}) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/status_test.go b/ui/status_test.go index f0d8d072..79f4c326 100644 --- a/ui/status_test.go +++ b/ui/status_test.go @@ -2,14 +2,10 @@ package ui import ( "bytes" - "github.com/algorandfoundation/hack-tui/internal" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/exp/golden" - "github.com/charmbracelet/x/exp/teatest" "testing" "time" + "github.com/algorandfoundation/hack-tui/internal" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" @@ -112,9 +108,6 @@ func Test_StatusMessages(t *testing.T) { teatest.WithDuration(time.Second*3), ) -<<<<<<< HEAD -<<<<<<< HEAD -======= // Send the state tm.Send(state) @@ -129,25 +122,8 @@ func Test_StatusMessages(t *testing.T) { Type: tea.KeyRunes, Runes: []rune("ctrl+c"), }) ->>>>>>> main // Send quit msg tm.Send(tea.QuitMsg{}) -======= - // Send the state - tm.Send(state) - - // Send hide key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("h"), - }) - - // Send quit key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("ctrl+c"), - }) ->>>>>>> 967fa6b (test: status snapshots and 100%) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden index f942f810..14627bb1 100644 --- a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden +++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden @@ -1,8 +1,4 @@ -<<<<<<< HEAD -╭──────────────────────────────────────────────────────────────────────────────╮ -======= ╭──Protocol────────────────────────────────────────────────────────────────────╮ ->>>>>>> main │ Node: v0.0.0-test │ │ │ │ Network: test-v1 │ diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden index 87910af8..5c48ef5e 100644 --- a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden +++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden @@ -1,8 +1,4 @@ -<<<<<<< HEAD -╭──────────────────────────────────────────────────────────────────────────────╮ -======= ╭──Protocol────────────────────────────────────────────────────────────────────╮ ->>>>>>> main │ Node: v0.0.0-test │ │ Network: test-v1 │ │ Protocol Voting: false │ diff --git a/ui/testdata/Test_ProtocolSnapshot/Visible.golden b/ui/testdata/Test_ProtocolSnapshot/Visible.golden index c06766db..e26f5535 100644 --- a/ui/testdata/Test_ProtocolSnapshot/Visible.golden +++ b/ui/testdata/Test_ProtocolSnapshot/Visible.golden @@ -1,8 +1,4 @@ -<<<<<<< HEAD -╭──────────────────────────────────────────────────────────────────────────────╮ -======= ╭──Protocol────────────────────────────────────────────────────────────────────╮ ->>>>>>> main │ Node: v0.0.0-test [UPDATE AVAILABLE] │ │ │ │ Network: test-v1 │ From 2b48b3402ac5c61b27bcb40c87ebdd83cd1b8c85 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:57:47 +0100 Subject: [PATCH 07/23] ci: only PR --- .github/workflows/node_test.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml index 4e62f691..e7812c9e 100644 --- a/.github/workflows/node_test.yaml +++ b/.github/workflows/node_test.yaml @@ -5,9 +5,6 @@ name: Node Command OS-Matrix Test on: workflow_dispatch: - push: - paths: - - "cmd/**" pull_request: paths: - "cmd/**" From 0e888c648b9f0174aa046b014f70bf28d4bbae0e Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:01:05 +0100 Subject: [PATCH 08/23] ci: use checkout action --- .github/workflows/node_test.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml index e7812c9e..31df5376 100644 --- a/.github/workflows/node_test.yaml +++ b/.github/workflows/node_test.yaml @@ -14,14 +14,15 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Setup Go uses: actions/setup-go@v4 with: go-version: 1.22 - name: Run Ubuntu commands run: | - git clone https://github.com/algorandfoundation/hack-tui.git - cd hack-tui go build . sudo ./hack-tui node install sudo ./hack-tui node start @@ -36,12 +37,14 @@ jobs: macos: runs-on: macos-latest steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Go + run: brew install go + - name: Run MacOs commands run: | - brew install go - cd ~/ - git clone https://github.com/algorandfoundation/hack-tui.git - cd hack-tui go build . sudo ./hack-tui node install sudo ./hack-tui node start From 55d72252c62043474107500549cdacead6fb0590 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:14:36 +0100 Subject: [PATCH 09/23] chore: ignore coverage.txt to avoid accidentally checking it in --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fce50c3a..ab058d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ bin # Test binary, built with `go test -c` *.test +# coverage.txt, in case you run tests locally +coverage.txt + # Output of the go coverage tool, specifically when used with LiteIDE *.out From e699915d51494e6abcc933cfcfe8cc2ff0ce1346 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:14:49 +0100 Subject: [PATCH 10/23] fix: newlines --- ui/pages/accounts/testdata/Test_Snapshot/Visible.golden | 2 +- ui/testdata/Test_ErrorSnapshot/Visible.golden | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden index d3bc0f03..03e6474b 100644 --- a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden +++ b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden @@ -37,4 +37,4 @@ │ │ │ │ │ │ -╰────( (g)enerate )─────────────────────────| (a)ccounts | (k)eys | (t)xn |────╯ +╰────( (g)enerate )─────────────────────────| (a)ccounts | (k)eys | (t)xn |────╯ \ No newline at end of file diff --git a/ui/testdata/Test_ErrorSnapshot/Visible.golden b/ui/testdata/Test_ErrorSnapshot/Visible.golden index 8b89731c..93d1e257 100644 --- a/ui/testdata/Test_ErrorSnapshot/Visible.golden +++ b/ui/testdata/Test_ErrorSnapshot/Visible.golden @@ -19,4 +19,4 @@ │ │ │ │ │ │ -╰─────────( Waiting for recovery... )────╯ +╰─────────( Waiting for recovery... )────╯ \ No newline at end of file From 3c9f104a64c5bda00835fc209f597985e4e282f4 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Mon, 25 Nov 2024 17:34:10 -0500 Subject: [PATCH 11/23] test(node): basic playbook and coverage strategy --- .docker/Fedora.dockerfile | 43 +++++++++++++++++++++++++++++ .docker/Ubuntu.dockerfile | 34 +++++++++++++++++++++++ .gitignore | 1 + Makefile | 4 +++ docker-compose.integration.yaml | 49 +++++++++++++++++++++++++++++++++ playbook.yaml | 21 ++++++++++++++ run_integraiton.sh | 9 ++++++ 7 files changed, 161 insertions(+) create mode 100644 .docker/Fedora.dockerfile create mode 100644 .docker/Ubuntu.dockerfile create mode 100644 docker-compose.integration.yaml create mode 100644 playbook.yaml create mode 100755 run_integraiton.sh diff --git a/.docker/Fedora.dockerfile b/.docker/Fedora.dockerfile new file mode 100644 index 00000000..305cdfc8 --- /dev/null +++ b/.docker/Fedora.dockerfile @@ -0,0 +1,43 @@ +FROM golang:1.23-bookworm as BUILDER + +WORKDIR /app + +ADD . . + +RUN go build -cover -o ./bin/algorun *.go + + +FROM fedora:39 as legacy + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun +RUN dnf install systemd ansible-core -y && \ + mkdir -p /app/coverage/int/fedora/39 && \ + echo GOCOVERDIR=/app/coverage/int/fedora/39 >> /etc/environment + +STOPSIGNAL SIGRTMIN+3 +CMD ["/usr/lib/systemd/systemd"] + +FROM fedora:40 as previous + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun + +RUN dnf install systemd ansible-core -y && \ + mkdir -p /app/coverage/int/fedora/40 && \ + echo GOCOVERDIR=/app/coverage/int/fedora/40 >> /etc/environment + +STOPSIGNAL SIGRTMIN+3 +CMD ["/usr/lib/systemd/systemd"] + +FROM fedora:41 as latest + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun + +RUN dnf install systemd ansible-core -y && \ + mkdir -p /app/coverage/int/fedora/41 && \ + echo GOCOVERDIR=/app/coverage/int/fedora/41 >> /etc/environment + +STOPSIGNAL SIGRTMIN+3 +CMD ["/usr/lib/systemd/systemd"] diff --git a/.docker/Ubuntu.dockerfile b/.docker/Ubuntu.dockerfile new file mode 100644 index 00000000..2f0312de --- /dev/null +++ b/.docker/Ubuntu.dockerfile @@ -0,0 +1,34 @@ +FROM golang:1.23-bookworm as BUILDER + +WORKDIR /app + +ADD . . + +RUN go build -cover -o ./bin/algorun *.go + + +FROM ubuntu:22.04 as jammy + +RUN apt-get update && apt-get install systemd software-properties-common -y && add-apt-repository --yes --update ppa:ansible/ansible + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun +RUN mkdir -p /app/coverage/int/ubuntu/22.04 && \ + echo GOCOVERDIR=/app/coverage/int/ubuntu/22.04 >> /etc/environment && \ + apt-get install ansible -y + +STOPSIGNAL SIGRTMIN+3 +CMD ["/usr/lib/systemd/systemd"] + +FROM ubuntu:24.04 as noble + +RUN apt-get update && apt-get install systemd software-properties-common -y && add-apt-repository --yes --update ppa:ansible/ansible + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun +RUN mkdir -p /app/coverage/int/ubuntu/24.04 && \ + echo GOCOVERDIR=/app/coverage/int/ubuntu/24.04 >> /etc/environment && \ + apt-get install ansible -y + +STOPSIGNAL SIGRTMIN+3 +CMD ["/usr/lib/systemd/systemd"] \ No newline at end of file diff --git a/.gitignore b/.gitignore index ab058d2c..baec94da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +coverage hack-tui bin .data diff --git a/Makefile b/Makefile index 40bbaa98..e87f96ff 100644 --- a/Makefile +++ b/Makefile @@ -4,3 +4,7 @@ test: go test -coverpkg=./... -covermode=atomic ./... generate: oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml +unit: + mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit +combine: + go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt \ No newline at end of file diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml new file mode 100644 index 00000000..0b495d47 --- /dev/null +++ b/docker-compose.integration.yaml @@ -0,0 +1,49 @@ +services: + ubuntu.22.04: + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/ubuntu/22.04 + build: + context: . + target: jammy + dockerfile: .docker/Ubuntu.dockerfile + ports: + - "2222:22" + volumes: + - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04" + ubuntu.24.04: + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/ubuntu/24.04 + build: + context: . + target: noble + dockerfile: .docker/Ubuntu.dockerfile + ports: + - "2223:22" + volumes: + - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04" + fedora.39: + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/fedora/39 + build: + context: . + target: legacy + dockerfile: .docker/Fedora.dockerfile + ports: + - "2224:22" + volumes: + - "./coverage/int/fedora/39:/app/coverage/int/fedora/39" + fedora.40: + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/fedora/40 + build: + context: . + target: previous + dockerfile: .docker/Fedora.dockerfile + ports: + - "2225:22" + volumes: + - "./coverage/int/fedora/40:/app/coverage/int/fedora/40" \ No newline at end of file diff --git a/playbook.yaml b/playbook.yaml new file mode 100644 index 00000000..3c2e0ae7 --- /dev/null +++ b/playbook.yaml @@ -0,0 +1,21 @@ +- name: Test Instance + hosts: localhost + tasks: + - name: Ensure algorun exists + stat: + path: /usr/bin/algorun + register: binpath + - name: Fail missing binary + fail: + msg: "Must have algorun installed!" + when: not binpath.stat.exists + - name: Run installer + command: algorun node install + - name: Run installer twice + command: algorun node install + - name: Run upgrade + command: algorun node upgrade + - name: Run stop + command: algorun node stop + - name: Run Start + command: algorun node start \ No newline at end of file diff --git a/run_integraiton.sh b/run_integraiton.sh new file mode 100755 index 00000000..48695ff4 --- /dev/null +++ b/run_integraiton.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +docker compose -f docker-compose.integration.yaml build --no-cache +docker compose -f docker-compose.integration.yaml up -d + +for testInstance in $(docker compose ps --services) +do + docker compose exec -it "$testInstance" ansible-playbook --connection=local -i localhost /root/playbook.yaml +done \ No newline at end of file From df73d85d441d8482cb07efd42e2d12598e21a586 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Mon, 25 Nov 2024 19:43:57 -0500 Subject: [PATCH 12/23] build(node): run integration code tests --- .github/workflows/code_test.yaml | 13 +++++++-- Makefile | 4 +-- docker-compose.integration.yaml | 48 ++++++++++++++++---------------- playbook.yaml | 3 +- run_integraiton.sh | 1 - 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml index cd6a095e..6e22cf12 100644 --- a/.github/workflows/code_test.yaml +++ b/.github/workflows/code_test.yaml @@ -55,8 +55,17 @@ jobs: - name: Build run: go build -o bin/algorun *.go - - name: Test with the Go CLI - run: go test ./... -coverprofile=./coverage.txt -covermode=atomic -coverpkg=./... + - name: Unit Tests + run: make unit + + - name: Kill docker + run: docker compose down + + - name: Integration tests + run: ./run_integration.sh + + - name: Combine coverage + run: make combine-coverage - name: Upload results to Codecov uses: codecov/codecov-action@v4 diff --git a/Makefile b/Makefile index e87f96ff..53dc959e 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,5 @@ generate: oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml unit: mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit -combine: - go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt \ No newline at end of file +combine-coverage: + go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt \ No newline at end of file diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml index 0b495d47..1456d818 100644 --- a/docker-compose.integration.yaml +++ b/docker-compose.integration.yaml @@ -1,16 +1,16 @@ services: - ubuntu.22.04: - privileged: true - environment: - - GOCOVERDIR=/app/coverage/int/ubuntu/22.04 - build: - context: . - target: jammy - dockerfile: .docker/Ubuntu.dockerfile - ports: - - "2222:22" - volumes: - - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04" +# ubuntu.22.04: +# privileged: true +# environment: +# - GOCOVERDIR=/app/coverage/int/ubuntu/22.04 +# build: +# context: . +# target: jammy +# dockerfile: .docker/Ubuntu.dockerfile +# ports: +# - "2222:22" +# volumes: +# - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04" ubuntu.24.04: privileged: true environment: @@ -23,18 +23,18 @@ services: - "2223:22" volumes: - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04" - fedora.39: - privileged: true - environment: - - GOCOVERDIR=/app/coverage/int/fedora/39 - build: - context: . - target: legacy - dockerfile: .docker/Fedora.dockerfile - ports: - - "2224:22" - volumes: - - "./coverage/int/fedora/39:/app/coverage/int/fedora/39" +# fedora.39: +# privileged: true +# environment: +# - GOCOVERDIR=/app/coverage/int/fedora/39 +# build: +# context: . +# target: legacy +# dockerfile: .docker/Fedora.dockerfile +# ports: +# - "2224:22" +# volumes: +# - "./coverage/int/fedora/39:/app/coverage/int/fedora/39" fedora.40: privileged: true environment: diff --git a/playbook.yaml b/playbook.yaml index 3c2e0ae7..db717bec 100644 --- a/playbook.yaml +++ b/playbook.yaml @@ -18,4 +18,5 @@ - name: Run stop command: algorun node stop - name: Run Start - command: algorun node start \ No newline at end of file + command: algorun node start + # TODO: start a private network, fund TUI account and run TUI integration \ No newline at end of file diff --git a/run_integraiton.sh b/run_integraiton.sh index 48695ff4..01eac615 100755 --- a/run_integraiton.sh +++ b/run_integraiton.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -docker compose -f docker-compose.integration.yaml build --no-cache docker compose -f docker-compose.integration.yaml up -d for testInstance in $(docker compose ps --services) From 4476c5a1be8e053a2def98f96db680a459888a0e Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Mon, 25 Nov 2024 19:54:24 -0500 Subject: [PATCH 13/23] build(node): convert integration to makefile --- .github/workflows/code_test.yaml | 2 +- Makefile | 5 +++++ run_integraiton.sh | 8 -------- 3 files changed, 6 insertions(+), 9 deletions(-) delete mode 100755 run_integraiton.sh diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml index 6e22cf12..4133f6e8 100644 --- a/.github/workflows/code_test.yaml +++ b/.github/workflows/code_test.yaml @@ -62,7 +62,7 @@ jobs: run: docker compose down - name: Integration tests - run: ./run_integration.sh + run: make integration - name: Combine coverage run: make combine-coverage diff --git a/Makefile b/Makefile index 53dc959e..2fdd62fe 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,10 @@ generate: oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml unit: mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit +integration: + docker compose -f docker-compose.integration.yaml up -d ; \ + for service in $(shell docker compose -f docker-compose.integration.yaml ps --services) ; do \ + docker compose exec -it "$$service" ansible-playbook --connection=local /root/playbook.yaml ; \ + done combine-coverage: go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt \ No newline at end of file diff --git a/run_integraiton.sh b/run_integraiton.sh deleted file mode 100755 index 01eac615..00000000 --- a/run_integraiton.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -docker compose -f docker-compose.integration.yaml up -d - -for testInstance in $(docker compose ps --services) -do - docker compose exec -it "$testInstance" ansible-playbook --connection=local -i localhost /root/playbook.yaml -done \ No newline at end of file From 26bf8d3741728f0a46a569cfafa194719929ab57 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Mon, 25 Nov 2024 20:28:40 -0500 Subject: [PATCH 14/23] build(node): move compose to action, wait for binds --- .github/workflows/code_test.yaml | 6 ++++++ Makefile | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml index 4133f6e8..df125f2e 100644 --- a/.github/workflows/code_test.yaml +++ b/.github/workflows/code_test.yaml @@ -61,6 +61,12 @@ jobs: - name: Kill docker run: docker compose down + - name: Start Integration + run: docker compose -f docker-compose.integration.yaml up -d + + - name: Wait for mount + run: npx wait-on ./coverage/int/fedora/40 ./coverage/int/ubuntu/24.04/ + - name: Integration tests run: make integration diff --git a/Makefile b/Makefile index 2fdd62fe..6a49a618 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,8 @@ generate: unit: mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit integration: - docker compose -f docker-compose.integration.yaml up -d ; \ for service in $(shell docker compose -f docker-compose.integration.yaml ps --services) ; do \ docker compose exec -it "$$service" ansible-playbook --connection=local /root/playbook.yaml ; \ done combine-coverage: - go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt \ No newline at end of file + go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/24.04,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt \ No newline at end of file From 244313073163bad7faf4a97b7bd2782c39e99bfb Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:37:17 +0100 Subject: [PATCH 15/23] feat: migrate to algorand foundation homebrew node tap --- cmd/node/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/node/install.go b/cmd/node/install.go index c0f08fee..4cb13b96 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -175,7 +175,7 @@ Algod is running in the background as a system-level service. func runHomebrewInstallCommandsAsUser(user string) error { homebrewCmds := [][]string{ - {"brew", "tap", "HashMapsData2Value/homebrew-tap"}, + {"brew", "tap", "algorandfoundation/homebrew-node"}, {"brew", "install", "algorand"}, {"brew", "--prefix", "algorand", "--installed"}, } From 5c26721c8474e3484d39472714cbcd74d21c94ca Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Tue, 26 Nov 2024 16:40:24 -0500 Subject: [PATCH 16/23] test(node): disable CGO, test Ubuntu 18.04 with apt disabled --- .docker/Fedora.dockerfile | 2 +- .docker/Ubuntu.dockerfile | 15 +++++++- Makefile | 2 +- docker-compose.integration.yaml | 65 +++++++++++++++++++-------------- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/.docker/Fedora.dockerfile b/.docker/Fedora.dockerfile index 305cdfc8..1891af84 100644 --- a/.docker/Fedora.dockerfile +++ b/.docker/Fedora.dockerfile @@ -4,7 +4,7 @@ WORKDIR /app ADD . . -RUN go build -cover -o ./bin/algorun *.go +RUN CGO_ENABLED=0 go build -cover -o ./bin/algorun *.go FROM fedora:39 as legacy diff --git a/.docker/Ubuntu.dockerfile b/.docker/Ubuntu.dockerfile index 2f0312de..f5cca2f7 100644 --- a/.docker/Ubuntu.dockerfile +++ b/.docker/Ubuntu.dockerfile @@ -4,8 +4,21 @@ WORKDIR /app ADD . . -RUN go build -cover -o ./bin/algorun *.go +RUN CGO_ENABLED=0 go build -cover -o ./bin/algorun *.go +FROM ubuntu:18.04 as bionic + +RUN apt-get update && apt-get install systemd software-properties-common -y && add-apt-repository --yes --update ppa:ansible/ansible + +ADD playbook.yaml /root/playbook.yaml +COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun +RUN mkdir -p /app/coverage/int/ubuntu/18.04 && \ + echo GOCOVERDIR=/app/coverage/int/ubuntu/18.04 >> /etc/environment && \ + apt-get install ansible -y && \ + chmod 0 /usr/bin/apt # Liam Neeson + +STOPSIGNAL SIGRTMIN+3 +CMD ["/bin/systemd"] FROM ubuntu:22.04 as jammy diff --git a/Makefile b/Makefile index 6a49a618..195d9e3a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ build: - go build -o bin/algorun *.go + CGO_ENABLED=0 go build -o bin/algorun *.go test: go test -coverpkg=./... -covermode=atomic ./... generate: diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml index 1456d818..9a89539d 100644 --- a/docker-compose.integration.yaml +++ b/docker-compose.integration.yaml @@ -1,16 +1,29 @@ services: -# ubuntu.22.04: -# privileged: true -# environment: -# - GOCOVERDIR=/app/coverage/int/ubuntu/22.04 -# build: -# context: . -# target: jammy -# dockerfile: .docker/Ubuntu.dockerfile -# ports: -# - "2222:22" -# volumes: -# - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04" + # Legacy with apt disabled + ubuntu.18.04: + deploy: + replicas: 0 + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/ubuntu/18.04 + build: + context: . + target: bionic + dockerfile: .docker/Ubuntu.dockerfile + volumes: + - "./coverage/int/ubuntu/18.04:/app/coverage/int/ubuntu/18.04" + ubuntu.22.04: + deploy: + replicas: 0 + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/ubuntu/22.04 + build: + context: . + target: jammy + dockerfile: .docker/Ubuntu.dockerfile + volumes: + - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04" ubuntu.24.04: privileged: true environment: @@ -19,22 +32,20 @@ services: context: . target: noble dockerfile: .docker/Ubuntu.dockerfile - ports: - - "2223:22" volumes: - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04" -# fedora.39: -# privileged: true -# environment: -# - GOCOVERDIR=/app/coverage/int/fedora/39 -# build: -# context: . -# target: legacy -# dockerfile: .docker/Fedora.dockerfile -# ports: -# - "2224:22" -# volumes: -# - "./coverage/int/fedora/39:/app/coverage/int/fedora/39" + fedora.39: + deploy: + replicas: 0 + privileged: true + environment: + - GOCOVERDIR=/app/coverage/int/fedora/39 + build: + context: . + target: legacy + dockerfile: .docker/Fedora.dockerfile + volumes: + - "./coverage/int/fedora/39:/app/coverage/int/fedora/39" fedora.40: privileged: true environment: @@ -43,7 +54,5 @@ services: context: . target: previous dockerfile: .docker/Fedora.dockerfile - ports: - - "2225:22" volumes: - "./coverage/int/fedora/40:/app/coverage/int/fedora/40" \ No newline at end of file From bae2d6f42466e5bdbdf07369486c1f7dd405706f Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Mon, 9 Dec 2024 22:59:05 -0500 Subject: [PATCH 17/23] refactor: algod commands --- cmd/{node => configure}/configure.go | 48 ++-- cmd/configure/service.go | 26 ++ cmd/configure/utils.go | 97 ++++++++ cmd/node/debug.go | 25 ++ cmd/node/install.go | 345 +------------------------- cmd/node/main.go | 21 -- cmd/node/node.go | 58 +++++ cmd/node/start.go | 117 +-------- cmd/node/stop.go | 115 ++------- cmd/node/uninstall.go | 157 +----------- cmd/node/upgrade.go | 175 +------------- cmd/node/utils.go | 341 -------------------------- cmd/root.go | 15 +- internal/algod/algod.go | 154 ++++++++++++ internal/algod/fallback/algod.go | 90 +++++++ internal/algod/linux/linux.go | 198 +++++++++++++++ internal/algod/mac/mac.go | 348 +++++++++++++++++++++++++++ internal/algod/utils/utils.go | 69 ++++++ internal/system/cmds.go | 106 ++++++++ internal/system/service.go | 16 ++ 20 files changed, 1280 insertions(+), 1241 deletions(-) rename cmd/{node => configure}/configure.go (91%) create mode 100644 cmd/configure/service.go create mode 100644 cmd/configure/utils.go create mode 100644 cmd/node/debug.go delete mode 100644 cmd/node/main.go create mode 100644 cmd/node/node.go delete mode 100644 cmd/node/utils.go create mode 100644 internal/algod/algod.go create mode 100644 internal/algod/fallback/algod.go create mode 100644 internal/algod/linux/linux.go create mode 100644 internal/algod/mac/mac.go create mode 100644 internal/algod/utils/utils.go create mode 100644 internal/system/cmds.go create mode 100644 internal/system/service.go diff --git a/cmd/node/configure.go b/cmd/configure/configure.go similarity index 91% rename from cmd/node/configure.go rename to cmd/configure/configure.go index a8746d5b..5b5d6336 100644 --- a/cmd/node/configure.go +++ b/cmd/configure/configure.go @@ -1,8 +1,11 @@ -package node +package configure import ( "bytes" "fmt" + "github.com/algorandfoundation/algorun-tui/cmd/node" + "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/internal/algod/utils" "os" "os/exec" "runtime" @@ -12,35 +15,34 @@ import ( "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() - }, +var Cmd = &cobra.Command{ + Use: "configure", + Short: "Configure Algod", + Long: "Configure Algod settings", + SilenceUsage: true, + PersistentPreRunE: node.NeedsToBeStopped, + //RunE: func(cmd *cobra.Command, args []string) error { + // return configureNode() + //}, } -// TODO: configure not just data directory but algod path -func configureNode() { - if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { - panic("Unsupported OS: " + runtime.GOOS) - } +func init() { + Cmd.AddCommand(serviceCmd) +} + +const ConfigureRunningErrorMsg = "algorand is currently running. Please stop the node with *node stop* before configuring" +// TODO: configure not just data directory but algod path +func configureNode() error { var systemServiceConfigure bool - if !isRunningWithSudo() { - fmt.Println("This command must be run with super-user priviledges (sudo).") - os.Exit(1) + if algod.IsRunning() { + return fmt.Errorf(ConfigureRunningErrorMsg) } // Check systemctl first - if checkAlgorandServiceCreated() { + if algod.IsService() { 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 { @@ -105,7 +107,7 @@ func configureNode() { } // Do quick "lazy" check for existing Algorand Data directories - paths := lazyCheckAlgorandDataDirs() + paths := utils.GetKnownDataPaths() if len(paths) != 0 { @@ -169,7 +171,7 @@ func configureNode() { } else { affectALGORAND_DATA(selectedPath) } - os.Exit(0) + return nil } func editAlgorandServiceFile(dataDirectoryPath string) { diff --git a/cmd/configure/service.go b/cmd/configure/service.go new file mode 100644 index 00000000..8a84ae75 --- /dev/null +++ b/cmd/configure/service.go @@ -0,0 +1,26 @@ +package configure + +import ( + "errors" + "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/spf13/cobra" +) + +var serviceCmd = &cobra.Command{ + Use: "service", + Short: "Configure the node service", + Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("Configure the service that runs the node."), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if !system.IsSudo() { + return errors.New( + "you need to be root to run this command. Please run this command with sudo") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: Combine this with algod.UpdateService and algod.SetNetwork + return algod.EnsureService() + }, +} diff --git a/cmd/configure/utils.go b/cmd/configure/utils.go new file mode 100644 index 00000000..16926921 --- /dev/null +++ b/cmd/configure/utils.go @@ -0,0 +1,97 @@ +package configure + +import ( + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "os" +) + +type Release struct { + Name string `json:"name"` + ZipballURL string `json:"zipball_url"` + TarballURL string `json:"tarball_url"` + Commit struct { + Sha string `json:"sha"` + URL string `json:"url"` + } `json:"commit"` + NodeID string `json:"node_id"` +} + +// Queries user on the provided prompt and returns the user input +func promptWrapperInput(promptLabel string) string { + prompt := promptui.Prompt{ + Label: promptLabel, + } + + result, err := prompt.Run() + cobra.CheckErr(err) + + return result +} + +// Queries user on the provided prompt and returns true if user inputs "y" +func promptWrapperYes(promptLabel string) bool { + return promptWrapperInput(promptLabel) == "y" +} + +// Queries user on the provided prompt and returns true if user does not input "y" +// Included for improved readability of decision tree, despite being redundant. +func promptWrapperNo(promptLabel string) bool { + return promptWrapperInput(promptLabel) != "y" +} + +// Queries user on the provided prompt and returns the selected item +func promptWrapperSelection(promptLabel string, items []string) string { + prompt := promptui.Select{ + Label: promptLabel, + Items: items, + } + + _, result, err := prompt.Run() + cobra.CheckErr(err) + + fmt.Printf("You selected: %s\n", result) + + return result +} + +// TODO: consider replacing with a method that does more for the user +func affectALGORAND_DATA(path string) { + fmt.Println("Please execute the following in your terminal to set the environment variable:") + fmt.Println("") + fmt.Println("export ALGORAND_DATA=" + path) + fmt.Println("") +} + +func validateAlgorandDataDir(path string) bool { + info, err := os.Stat(path) + + // Check if the path exists + if os.IsNotExist(err) { + return false + } + + // Check if the path is a directory + if !info.IsDir() { + return false + } + + paths := system.FindPathToFile(path, "algod.token") + if len(paths) == 1 { + return true + } + return false +} + +// Checks if Algorand data directories exist, based off of existence of the "algod.token" file +func deepSearchAlgorandDataDirs() []string { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // TODO: consider a better way to identify an Algorand data directory + paths := system.FindPathToFile(home, "algod.token") + + return paths +} diff --git a/cmd/node/debug.go b/cmd/node/debug.go new file mode 100644 index 00000000..80f36409 --- /dev/null +++ b/cmd/node/debug.go @@ -0,0 +1,25 @@ +package node + +import ( + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/internal/algod/utils" + "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/spf13/cobra" +) + +var debugCmd = &cobra.Command{ + Use: "debug", + Short: "Display debug information for developers", + Long: "Prints debug data to be copy and pasted to a bug report.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + paths := utils.GetKnownDataPaths() + fmt.Printf("Algod in PATH: %v\n", system.CmdExists("algod")) + fmt.Printf("Algod is installed: %v\n", algod.IsInstalled()) + fmt.Printf("Algod is running: %v\n", algod.IsRunning()) + fmt.Printf("Algod is service: %v\n", algod.IsService()) + fmt.Printf("Algod paths: %+v\n", paths) + return nil + }, +} diff --git a/cmd/node/install.go b/cmd/node/install.go index 4cb13b96..051631c1 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -2,341 +2,22 @@ package node import ( "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - + "github.com/algorandfoundation/algorun-tui/internal/algod" "github.com/spf13/cobra" ) +const InstallExistsMsg = "algod is already installed" + 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() + Use: "install", + Short: "Install Algorand node (Algod)", + Long: "Install Algorand node (Algod) and other binaries on your system", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Checking if Algod is installed...") + if algod.IsInstalled() { + return fmt.Errorf(InstallExistsMsg) + } + return algod.Install() }, } - -func installNode() { - fmt.Println("Checking if Algod is installed...") - - // 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 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") - - 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", "enable", "algorand.service"}, - {"systemctl", "start", "algorand.service"}, - {"rm", "-f", "rpm_algorand.pub"}, - } - } 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", "enable", "algorand.service"}, - {"systemctl", "start", "algorand.service"}, - {"rm", "-f", "rpm_algorand.pub"}, - } - } 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...") - - // 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", "algorandfoundation/homebrew-node"}, - {"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(`<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>Label</key> - <string>com.algorand.algod</string> - <key>ProgramArguments</key> - <array> - <string>%s/bin/algod</string> - <string>-d</string> - <string>%s/.algorand</string> - </array> - <key>RunAtLoad</key> - <true/> - <key>KeepAlive</key> - <true/> - <key>StandardOutPath</key> - <string>/tmp/algod.out</string> - <key>StandardErrorPath</key> - <string>/tmp/algod.err</string> -</dict> -</plist>`, 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 deleted file mode 100644 index 18af211b..00000000 --- a/cmd/node/main.go +++ /dev/null @@ -1,21 +0,0 @@ -package node - -import ( - "github.com/algorandfoundation/algorun-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/node.go b/cmd/node/node.go new file mode 100644 index 00000000..4741c2ea --- /dev/null +++ b/cmd/node/node.go @@ -0,0 +1,58 @@ +package node + +import ( + "errors" + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/spf13/cobra" + "os" + "runtime" +) + +const PermissionErrorMsg = "this command must be run with super-user privileges (sudo)" +const NotInstalledErrorMsg = "algod is not installed. please run the *node install* command" +const RunningErrorMsg = "algod is running, please run the *node stop* command" +const NotRunningErrorMsg = "algod is not running" + +var Cmd = &cobra.Command{ + Use: "node", + Short: "Node Management", + Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Check that we are calling with sudo on linux + if os.Geteuid() != 0 && runtime.GOOS == "linux" { + return errors.New(PermissionErrorMsg) + } + return nil + }, +} + +func NeedsToBeRunning(cmd *cobra.Command, args []string) error { + if !algod.IsInstalled() { + return fmt.Errorf(NotInstalledErrorMsg) + } + if !algod.IsRunning() { + return fmt.Errorf(NotRunningErrorMsg) + } + return nil +} + +func NeedsToBeStopped(cmd *cobra.Command, args []string) error { + if !algod.IsInstalled() { + return fmt.Errorf(NotInstalledErrorMsg) + } + if algod.IsRunning() { + return fmt.Errorf(RunningErrorMsg) + } + return nil +} + +func init() { + Cmd.AddCommand(installCmd) + Cmd.AddCommand(startCmd) + Cmd.AddCommand(stopCmd) + Cmd.AddCommand(uninstallCmd) + Cmd.AddCommand(upgradeCmd) + Cmd.AddCommand(debugCmd) +} diff --git a/cmd/node/start.go b/cmd/node/start.go index e7d4092d..0691e615 100644 --- a/cmd/node/start.go +++ b/cmd/node/start.go @@ -1,118 +1,17 @@ package node import ( - "fmt" - "os" - "os/exec" - "runtime" - "syscall" - "time" - + "github.com/algorandfoundation/algorun-tui/internal/algod" "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() + Use: "start", + Short: "Start Algod", + Long: "Start Algod on your system (the one on your PATH).", + SilenceUsage: true, + PersistentPreRunE: NeedsToBeStopped, + RunE: func(cmd *cobra.Command, args []string) error { + return algod.Start() }, } - -// 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 index fe5180ad..02a6a5c0 100644 --- a/cmd/node/stop.go +++ b/cmd/node/stop.go @@ -2,110 +2,35 @@ package node import ( "fmt" - "os" - "os/exec" - "runtime" - "syscall" + "github.com/algorandfoundation/algorun-tui/internal/algod" "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) - } +const StopTimeout = 5 * time.Second +const StopSuccessMsg = "Algod stopped successfully" +const StopFailureMsg = "failed to stop Algod" - // 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) +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "Stop Algod", + Long: "Stop the Algod process on your system.", + SilenceUsage: true, + PersistentPreRunE: NeedsToBeRunning, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Stopping Algod...") + err := algod.Stop() if err != nil { - fmt.Printf("Failed to find process with PID %d: %v\n", pid, err) - cobra.CheckErr(err) + return fmt.Errorf(StopFailureMsg) } + time.Sleep(StopTimeout) - 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) + if algod.IsRunning() { + return fmt.Errorf(StopFailureMsg) } - 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.") + fmt.Println(StopSuccessMsg) + return nil + }, } diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go index 99834409..9a60b86f 100644 --- a/cmd/node/uninstall.go +++ b/cmd/node/uninstall.go @@ -1,158 +1,17 @@ package node import ( - "fmt" - "os" - "os/exec" - "runtime" - "strings" - + "github.com/algorandfoundation/algorun-tui/internal/algod" "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() + Use: "uninstall", + Short: "Uninstall Algorand node (Algod)", + Long: "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.", + SilenceUsage: true, + PersistentPreRunE: NeedsToBeStopped, + RunE: func(cmd *cobra.Command, args []string) error { + return algod.Uninstall() }, } - -// 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) - } - - // Check if Algod is running - if isAlgodRunning() { - fmt.Println("Algod is running. Please run *node stop* first to stop it.") - os.Exit(1) - } - - fmt.Println("Algod is installed. Proceeding...") - - // 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() { - - var unInstallCmds [][]string - - if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager - fmt.Println("Using apt package manager") - unInstallCmds = [][]string{ - {"apt", "remove", "algorand-devtools", "-y"}, - {"apt", "autoremove", "-y"}, - } - } else if checkCmdToolExists("apt-get") { - fmt.Println("Using apt-get package manager") - unInstallCmds = [][]string{ - {"apt-get", "remove", "algorand-devtools", "-y"}, - {"apt-get", "autoremove", "-y"}, - } - } else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager - fmt.Println("Using dnf package manager") - unInstallCmds = [][]string{ - {"dnf", "remove", "algorand-devtools", "-y"}, - } - } else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager - fmt.Println("Using yum package manager") - unInstallCmds = [][]string{ - {"yum", "remove", "algorand-devtools", "-y"}, - } - } else { - fmt.Println("Could not find a package manager to uninstall Algorand.") - os.Exit(1) - } - - // Commands to clear systemd algorand.service and any other files, like the configuration override - unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "sudo rm -rf /etc/systemd/system/algorand*"}) - unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"}) - - // Run each installation command - for _, cmdArgs := range unInstallCmds { - 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) - } - } - - // Check the status of the algorand service - cmd := exec.Command("systemctl", "status", "algorand") - output, err := cmd.CombinedOutput() - if err != nil && strings.Contains(string(output), "Unit algorand.service could not be found.") { - fmt.Println("Algorand service has been successfully removed.") - } else { - fmt.Printf("Failed to verify Algorand service uninstallation: %v\n", err) - fmt.Printf("Output: %s\n", string(output)) - os.Exit(1) - } - - fmt.Println("Algorand successfully uninstalled.") -} diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go index 55210b09..ee675686 100644 --- a/cmd/node/upgrade.go +++ b/cmd/node/upgrade.go @@ -1,175 +1,18 @@ package node import ( - "fmt" - "os" - "os/exec" - "runtime" - "strings" - + "github.com/algorandfoundation/algorun-tui/internal/algod" "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() + Use: "upgrade", + Short: "Upgrade Algod", + Long: "Upgrade Algod (if installed with package manager).", + SilenceUsage: true, + PersistentPreRunE: NeedsToBeStopped, + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: Check Version from S3 against the local binary + return algod.Update() }, } - -// 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/node/utils.go b/cmd/node/utils.go deleted file mode 100644 index e15763e1..00000000 --- a/cmd/node/utils.go +++ /dev/null @@ -1,341 +0,0 @@ -package node - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" -) - -type Release struct { - Name string `json:"name"` - ZipballURL string `json:"zipball_url"` - TarballURL string `json:"tarball_url"` - Commit struct { - Sha string `json:"sha"` - URL string `json:"url"` - } `json:"commit"` - NodeID string `json:"node_id"` -} - -// Queries user on the provided prompt and returns the user input -func promptWrapperInput(promptLabel string) string { - prompt := promptui.Prompt{ - Label: promptLabel, - } - - result, err := prompt.Run() - cobra.CheckErr(err) - - return result -} - -// Queries user on the provided prompt and returns true if user inputs "y" -func promptWrapperYes(promptLabel string) bool { - return promptWrapperInput(promptLabel) == "y" -} - -// Queries user on the provided prompt and returns true if user does not input "y" -// Included for improved readability of decision tree, despite being redundant. -func promptWrapperNo(promptLabel string) bool { - return promptWrapperInput(promptLabel) != "y" -} - -// Queries user on the provided prompt and returns the selected item -func promptWrapperSelection(promptLabel string, items []string) string { - prompt := promptui.Select{ - Label: promptLabel, - Items: items, - } - - _, result, err := prompt.Run() - cobra.CheckErr(err) - - fmt.Printf("You selected: %s\n", result) - - return result -} - -// Check if Algod is installed -func isAlgodInstalled() bool { - if runtime.GOOS == "windows" { - panic("Windows is not supported.") - } - - return checkCmdToolExists("algod") -} - -// Checks that a bash cli/cmd tool exists -func checkCmdToolExists(tool string) bool { - _, err := exec.LookPath(tool) - return err == nil -} - -// Find where algod is defined and print its version -func printAlgodInfo() { - algodPath, err := exec.LookPath("algod") - if err != nil { - fmt.Printf("Error finding algod: %v\n", err) - return - } - - // Get algod version - algodVersion, err := exec.Command("algod", "-v").Output() - if err != nil { - fmt.Printf("Error getting algod version: %v\n", err) - return - } - - fmt.Printf("Algod is located at: %s\n", algodPath) - fmt.Printf("algod -v\n") - fmt.Printf("%s\n", algodVersion) -} - -// TODO: consider replacing with a method that does more for the user -func affectALGORAND_DATA(path string) { - fmt.Println("Please execute the following in your terminal to set the environment variable:") - fmt.Println("") - fmt.Println("export ALGORAND_DATA=" + path) - fmt.Println("") -} - -// Check if the program is running with admin (super-user) priviledges -func isRunningWithSudo() bool { - return os.Geteuid() == 0 -} - -// Finds path(s) to a file in a directory and its subdirectories using parallel processing -func findPathToFile(startDir string, targetFileName string) []string { - var dirPaths []string - var mu sync.Mutex - var wg sync.WaitGroup - - fileChan := make(chan string) - - // Worker function to process files - worker := func() { - defer wg.Done() - for path := range fileChan { - info, err := os.Stat(path) - if err != nil { - continue - } - if !info.IsDir() && info.Name() == targetFileName { - dirPath := filepath.Dir(path) - mu.Lock() - dirPaths = append(dirPaths, dirPath) - mu.Unlock() - } - } - } - - // Start worker goroutines - numWorkers := 4 // Adjust the number of workers based on your system's capabilities - for i := 0; i < numWorkers; i++ { - wg.Add(1) - go worker() - } - - // Walk the directory tree and send file paths to the channel - err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - // Ignore permission errors - if os.IsPermission(err) { - return nil - } - return err - } - fileChan <- path - return nil - }) - - close(fileChan) - wg.Wait() - - if err != nil { - panic(err) - } - - return dirPaths -} - -func validateAlgorandDataDir(path string) bool { - info, err := os.Stat(path) - - // Check if the path exists - if os.IsNotExist(err) { - return false - } - - // Check if the path is a directory - if !info.IsDir() { - return false - } - - paths := findPathToFile(path, "algod.token") - if len(paths) == 1 { - return true - } - return false -} - -// Does a lazy check for Algorand data directories, based off of known common paths -func lazyCheckAlgorandDataDirs() []string { - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // Hardcoded paths known to be common Algorand data directories - commonAlgorandDataDirPaths := []string{ - "/var/lib/algorand", - filepath.Join(home, "node", "data"), - filepath.Join(home, ".algorand"), - } - - var paths []string - - for _, path := range commonAlgorandDataDirPaths { - if validateAlgorandDataDir(path) { - paths = append(paths, path) - } - } - - return paths -} - -// Checks if Algorand data directories exist, based off of existence of the "algod.token" file -func deepSearchAlgorandDataDirs() []string { - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // TODO: consider a better way to identify an Algorand data directory - paths := findPathToFile(home, "algod.token") - - return paths -} - -func findAlgodPID() (int, error) { - cmd := exec.Command("pgrep", "algod") - output, err := cmd.Output() - if err != nil { - return 0, err - } - - var pid int - _, err = fmt.Sscanf(string(output), "%d", &pid) - if err != nil { - return 0, fmt.Errorf("failed to parse PID: %v", err) - } - - return pid, nil -} - -// 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 - err := cmd.Run() - if err != nil { - return false - } - return strings.Contains(out.String(), "algorand.service") -} - -// 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 - err := cmd.Run() - if err != nil { - return false - } - 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") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, prefix) { - return strings.TrimSpace(strings.TrimPrefix(line, prefix)) - } - } - 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 df55723e..2e03a8d9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/algorandfoundation/algorun-tui/api" + "github.com/algorandfoundation/algorun-tui/cmd/configure" "github.com/algorandfoundation/algorun-tui/cmd/node" "github.com/algorandfoundation/algorun-tui/internal" "github.com/algorandfoundation/algorun-tui/ui" @@ -18,6 +19,7 @@ import ( "github.com/spf13/viper" "io" "os" + "runtime" "strings" ) @@ -137,15 +139,15 @@ func init() { rootCmd.Version = Version // Bindings - rootCmd.PersistentFlags().StringVarP(&algod, "algod-endpoint", "a", "", style.LightBlue("algod endpoint address URI, including http[s]")) - rootCmd.PersistentFlags().StringVarP(&token, "algod-token", "t", "", lipgloss.JoinHorizontal( + rootCmd.Flags().StringVarP(&algod, "algod-endpoint", "a", "", style.LightBlue("algod endpoint address URI, including http[s]")) + rootCmd.Flags().StringVarP(&token, "algod-token", "t", "", lipgloss.JoinHorizontal( lipgloss.Left, style.LightBlue("algod "), style.BoldUnderline("admin"), style.LightBlue(" token"), )) - _ = viper.BindPFlag("algod-endpoint", rootCmd.PersistentFlags().Lookup("algod-endpoint")) - _ = viper.BindPFlag("algod-token", rootCmd.PersistentFlags().Lookup("algod-token")) + _ = viper.BindPFlag("algod-endpoint", rootCmd.Flags().Lookup("algod-endpoint")) + _ = viper.BindPFlag("algod-token", rootCmd.Flags().Lookup("algod-token")) // Update Long Text rootCmd.Long += @@ -159,7 +161,10 @@ func init() { // Add Commands rootCmd.AddCommand(statusCmd) - rootCmd.AddCommand(node.NodeCmd) + if runtime.GOOS != "windows" { + rootCmd.AddCommand(node.Cmd) + rootCmd.AddCommand(configure.Cmd) + } } // Execute executes the root command. diff --git a/internal/algod/algod.go b/internal/algod/algod.go new file mode 100644 index 00000000..e19737f6 --- /dev/null +++ b/internal/algod/algod.go @@ -0,0 +1,154 @@ +package algod + +import ( + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod/linux" + "github.com/algorandfoundation/algorun-tui/internal/algod/mac" + "github.com/algorandfoundation/algorun-tui/internal/system" + "os/exec" + "runtime" +) + +const UnsupportedOSError = "unsupported operating system" + +// IsInstalled checks if the Algod software is installed on the system +// by verifying its presence and service setup. +func IsInstalled() bool { + //If algod is not in the path + if !system.CmdExists("algod") { + return false + } + // If the service is listed + switch runtime.GOOS { + case "linux": + return linux.IsService() + case "darwin": + return mac.IsService() + default: + fmt.Println("Unsupported operating system.") + return false + } +} + +// IsRunning checks if the algod is currently running on the host operating system. +// It returns true if the application is running, or false if it is not or if an error occurs. +// This function supports Linux and macOS platforms. It returns an error for unsupported operating systems. +func IsRunning() bool { + switch runtime.GOOS { + case "linux", "darwin": + fmt.Println("Checking if algod is running...") + err := exec.Command("pgrep", "algod").Run() + return err == nil + default: + return false + } +} + +// IsService determines if the Algorand service is configured as +// a system service on the current operating system. +func IsService() bool { + switch runtime.GOOS { + case "linux": + return linux.IsService() + case "darwin": + return mac.IsService() + default: + return false + } +} + +// SetNetwork configures the network to the specified setting +// or returns an error on unsupported operating systems. +func SetNetwork(network string) error { + return fmt.Errorf(UnsupportedOSError) +} + +// Install installs Algorand software based on the host OS +// and returns an error if the installation fails or is unsupported. +func Install() error { + switch runtime.GOOS { + case "linux": + return linux.Install() + case "darwin": + return mac.Install() + default: + return fmt.Errorf(UnsupportedOSError) + } +} + +// Update checks the operating system and performs an +// upgrade using OS-specific package managers, if supported. +func Update() error { + switch runtime.GOOS { + case "linux": + return linux.Upgrade() + case "darwin": + return mac.Upgrade() + default: + return fmt.Errorf(UnsupportedOSError) + } +} + +// Uninstall removes the Algorand software from the system based +// on the host operating system using appropriate methods. +func Uninstall() error { + switch runtime.GOOS { + case "linux": + return linux.Uninstall() + case "darwin": + return mac.Uninstall() + default: + return fmt.Errorf(UnsupportedOSError) + } +} + +// UpdateService updates the service configuration for the +// Algorand daemon based on the OS and reloads the service. +func UpdateService(dataDirectoryPath string) error { + switch runtime.GOOS { + case "linux": + return linux.UpdateService(dataDirectoryPath) + case "darwin": + return mac.UpdateService(dataDirectoryPath) + default: + return fmt.Errorf(UnsupportedOSError) + } +} + +// EnsureService ensures the `algod` service is configured and running +// as a service based on the OS; +// Returns an error for unsupported systems. +func EnsureService() error { + switch runtime.GOOS { + case "darwin": + return mac.EnsureService() + default: + return fmt.Errorf(UnsupportedOSError) + } +} + +// Start attempts to initiate the Algod service based on the +// host operating system. Returns an error for unsupported OS. +func Start() error { + switch runtime.GOOS { + case "linux": + return linux.Start() + case "darwin": + return mac.Start() + default: // Unsupported OS + return fmt.Errorf(UnsupportedOSError) + } +} + +// Stop shuts down the Algorand algod system process based on the current operating system. +// Returns an error if the operation fails or the operating system is unsupported. +func Stop() error { + switch runtime.GOOS { + case "linux": + return linux.Stop() + case "darwin": + return mac.Stop() + default: + return fmt.Errorf(UnsupportedOSError) + } +} diff --git a/internal/algod/fallback/algod.go b/internal/algod/fallback/algod.go new file mode 100644 index 00000000..e0ff3dc7 --- /dev/null +++ b/internal/algod/fallback/algod.go @@ -0,0 +1,90 @@ +package fallback + +import ( + "errors" + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod/utils" + "github.com/algorandfoundation/algorun-tui/internal/system" + "os" + "os/exec" + "syscall" +) + +func isRunning() (bool, error) { + return false, errors.New("not implemented") +} +func Install() error { + return system.RunAll(system.CmdsList{ + {"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"}, + }) +} + +func Start() error { + // 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 !utils.IsDataDir(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 { + return fmt.Errorf("Failed to start algod: %v\n", err) + } + return nil +} + +func Stop() error { + // 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 { + return fmt.Errorf("Failed to find algod process: %v\n", err) + } + + // Send SIGTERM to the process + process, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("Failed to find process with PID %d: %v\n", pid, err) + } + + err = process.Signal(syscall.SIGTERM) + if err != nil { + return fmt.Errorf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err) + } + + fmt.Println("Sent SIGTERM to algod process.") + return nil +} + +func findAlgodPID() (int, error) { + cmd := exec.Command("pgrep", "algod") + output, err := cmd.Output() + if err != nil { + return 0, err + } + + var pid int + _, err = fmt.Sscanf(string(output), "%d", &pid) + if err != nil { + return 0, fmt.Errorf("failed to parse PID: %v", err) + } + + return pid, nil +} diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go new file mode 100644 index 00000000..acd61804 --- /dev/null +++ b/internal/algod/linux/linux.go @@ -0,0 +1,198 @@ +package linux + +import ( + "bytes" + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod/fallback" + "github.com/algorandfoundation/algorun-tui/internal/system" + "log" + "os" + "os/exec" + "strings" + "text/template" +) + +const PackageManagerNotFoundMsg = "could not find a package manager to uninstall Algorand" + +type Algod struct { + system.Interface + Path string + DataDirectoryPath string +} + +// Install installs Algorand development tools or node software depending on the package manager. +func Install() error { + log.Println("Installing Algod on Linux") + // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager + if system.CmdExists("apt-get") { // On some Debian systems we use apt-get + log.Printf("Installing with apt-get") + return system.RunAll(system.CmdsList{ + {"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"}, + }) + } + + if system.CmdExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager + log.Printf("Installing with dnf") + return system.RunAll(system.CmdsList{ + {"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", "enable", "algorand.service"}, + {"systemctl", "start", "algorand.service"}, + {"rm", "-f", "rpm_algorand.pub"}, + }) + + } + + // TODO: watch this method to see if it is ever used + return fallback.Install() +} + +// Uninstall removes the Algorand software using a supported package manager or clears related system files if necessary. +// Returns an error if a supported package manager is not found or if any command fails during execution. +func Uninstall() error { + fmt.Println("Uninstalling Algorand") + var unInstallCmds system.CmdsList + // On Ubuntu and Debian there's the apt package manager + if system.CmdExists("apt-get") { + fmt.Println("Using apt-get package manager") + unInstallCmds = [][]string{ + {"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"}, + } + } + // On Fedora and CentOs8 there's the dnf package manager + if system.CmdExists("dnf") { + fmt.Println("Using dnf package manager") + unInstallCmds = [][]string{ + {"dnf", "remove", "algorand-devtools", "algorand", "-y"}, + } + } + // Error on unsupported package managers + if len(unInstallCmds) == 0 { + return fmt.Errorf(PackageManagerNotFoundMsg) + } + + // Commands to clear systemd algorand.service and any other files, like the configuration override + unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "rm -rf /etc/systemd/system/algorand*"}) + unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"}) + + return system.RunAll(unInstallCmds) +} + +// Upgrade updates Algorand and its dev tools using an approved package +// manager if available, otherwise returns an error. +func Upgrade() error { + if system.CmdExists("apt-get") { + return system.RunAll(system.CmdsList{ + {"apt-get", "update"}, + {"apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"}, + }) + } + if system.CmdExists("dnf") { + return system.RunAll(system.CmdsList{ + {"dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"}, + }) + } + return fmt.Errorf("the *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade") +} + +// Start attempts to start the Algorand service using the system's service manager. +// It executes the appropriate command for systemd on Linux-based systems. +// Returns an error if the command fails. +// TODO: Replace with D-Bus integration +func Start() error { + return exec.Command("systemctl", "start", "algorand").Run() +} + +// Stop shuts down the Algorand algod system process on Linux using the systemctl stop command. +// Returns an error if the operation fails. +// TODO: Replace with D-Bus integration +func Stop() error { + return exec.Command("systemctl", "stop", "algorand").Run() +} + +// IsService checks if the "algorand.service" is listed as a systemd unit file on Linux. +// Returns true if it exists. +// TODO: Replace with D-Bus integration +func IsService() bool { + out, err := system.Run([]string{"systemctl", "list-unit-files", "algorand.service"}) + if err != nil { + return false + } + return strings.Contains(out, "algorand.service") +} + +// UpdateService updates the systemd service file for the Algorand daemon +// with a new data directory path and reloads the daemon. +func UpdateService(dataDirectoryPath string) error { + + 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.") + + return nil +} diff --git a/internal/algod/mac/mac.go b/internal/algod/mac/mac.go new file mode 100644 index 00000000..f26eb7cc --- /dev/null +++ b/internal/algod/mac/mac.go @@ -0,0 +1,348 @@ +package mac + +import ( + "bytes" + "errors" + "fmt" + "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/spf13/cobra" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +// IsService 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 IsService() bool { + _, err := system.Run([]string{"launchctl", "list", "com.algorand.algod"}) + return err == nil +} + +// Install sets up Algod on macOS using Homebrew, +// configures necessary directories, and ensures it +// runs as a background service. +func Install() error { + fmt.Println("Installing Algod on macOS...") + + // Homebrew is our package manager of choice + if !system.CmdExists("brew") { + return fmt.Errorf("could not find Homebrew installed. Please install Homebrew and try again") + } + + err := system.RunAll(system.CmdsList{ + {"brew", "tap", "algorandfoundation/homebrew-node"}, + {"brew", "install", "algorand"}, + {"brew", "--prefix", "algorand", "--installed"}, + }) + if err != nil { + return err + } + + // Handle data directory and genesis.json file + handleDataDirMac() + + path, err := os.Executable() + if err != nil { + return err + } + // Create and load the launchd service + _, err = system.Run([]string{"sudo", path, "configure", "service"}) + if err != nil { + return fmt.Errorf("failed to create and load launchd service: %v\n", err) + } + + // Ensure Homebrew bin directory is in the PATH + // So that brew installed algorand binaries can be found + ensureHomebrewPathInEnv() + + if !IsService() { + return fmt.Errorf("algod unexpectedly NOT in path. Installation failed") + } + + fmt.Println(`Installed Algorand (Algod) with Homebrew. +Algod is running in the background as a system-level service. + `) + + return nil +} + +// Uninstall removes the Algorand application from the system using Homebrew if it is installed. +func Uninstall() error { + if !system.CmdExists("brew") { + return errors.New("homebrew is not installed") + } + return exec.Command("brew", "uninstall", "algorand").Run() +} + +// Upgrade updates the installed Algorand package using Homebrew if it's available and properly configured. +func Upgrade() error { + if !system.CmdExists("brew") { + return errors.New("homebrew is not installed") + } + // Check if algorand is installed with Homebrew + checkCmdArgs := system.CmdsList{{"brew", "--prefix", "algorand", "--installed"}} + if system.IsSudo() { + checkCmdArgs = checkCmdArgs.Su(os.Getenv("SUDO_USER")) + } + err := system.RunAll(checkCmdArgs) + if err != nil { + return err + } + // Upgrade algorand + upgradeCmdArgs := system.CmdsList{{"brew", "upgrade", "algorand", "--formula"}} + if system.IsSudo() { + upgradeCmdArgs = upgradeCmdArgs.Su(os.Getenv("SUDO_USER")) + } + return system.RunAll(upgradeCmdArgs) +} + +// Start algorand with launchd +func Start() error { + return exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist").Run() +} + +// Stop shuts down the Algorand algod system process using the launchctl bootout command. +// Returns an error if the operation fails. +func Stop() error { + return exec.Command("launchctl", "bootout", "system/com.algorand.algod").Run() +} + +// UpdateService updates the Algorand launchd service with +// a new data directory path and reloads the service configuration. +func UpdateService(dataDirectoryPath string) error { + + 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 := `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <dict> + <key>Label</key> + <string>com.algorand.algod</string> + <key>ProgramArguments</key> + <array> + <string>{{.AlgodPath}}</string> + <string>-d</string> + <string>{{.DataDirectoryPath}}</string> + </array> + <key>RunAtLoad</key> + <true/> + <key>KeepAlive</key> + <true/> + <key>StandardOutPath</key> + <string>/tmp/algod.out</string> + <key>StandardErrorPath</key> + <string>/tmp/algod.err</string> + </dict> + </plist>` + + // 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.") + 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 EnsureService() error { + // 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(`<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>com.algorand.algod</string> + <key>ProgramArguments</key> + <array> + <string>%s/bin/algod</string> + <string>-d</string> + <string>%s/.algorand</string> + </array> + <key>RunAtLoad</key> + <true/> + <key>KeepAlive</key> + <true/> + <key>StandardOutPath</key> + <string>/tmp/algod.out</string> + <key>StandardErrorPath</key> + <string>/tmp/algod.err</string> +</dict> +</plist>`, 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.") + + return nil +} + +// 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/internal/algod/utils/utils.go b/internal/algod/utils/utils.go new file mode 100644 index 00000000..a32de05d --- /dev/null +++ b/internal/algod/utils/utils.go @@ -0,0 +1,69 @@ +package utils + +import ( + "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/spf13/cobra" + "os" + "path/filepath" +) + +func IsDataDir(path string) bool { + info, err := os.Stat(path) + + // Check if the path exists + if os.IsNotExist(err) { + return false + } + + // Check if the path is a directory + if !info.IsDir() { + return false + } + + paths := system.FindPathToFile(path, "algod.token") + if len(paths) == 1 { + return true + } + return false +} + +func GetKnownPaths() []string { + // Hardcoded paths known to be common Algorand data directories + binPaths := []string{ + "/opt/homebrew/bin/algod", + "/opt/homebrew/bin/algod", + } + + var paths []string + + for _, path := range binPaths { + if IsDataDir(path) { + paths = append(paths, path) + } + } + + return paths +} + +// GetKnownDataPaths Does a lazy check for Algorand data directories, based off of known common paths +func GetKnownDataPaths() []string { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Hardcoded paths known to be common Algorand data directories + commonAlgorandDataDirPaths := []string{ + "/var/lib/algorand", + filepath.Join(home, "node", "data"), + filepath.Join(home, ".algorand"), + } + + var paths []string + + for _, path := range commonAlgorandDataDirPaths { + if IsDataDir(path) { + paths = append(paths, path) + } + } + + return paths +} diff --git a/internal/system/cmds.go b/internal/system/cmds.go new file mode 100644 index 00000000..02f5c633 --- /dev/null +++ b/internal/system/cmds.go @@ -0,0 +1,106 @@ +package system + +import ( + "fmt" + "github.com/algorandfoundation/algorun-tui/ui/style" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" +) + +func IsSudo() bool { + return os.Geteuid() == 0 +} + +// CmdExists checks that a bash cli/cmd tool exists +func CmdExists(tool string) bool { + _, err := exec.LookPath(tool) + return err == nil +} + +type CmdsList [][]string + +func (l CmdsList) Su(user string) CmdsList { + for i, args := range l { + if !strings.HasPrefix(args[0], "sudo") { + l[i] = append([]string{"sudo", "-u", user}, args...) + } + } + return l +} + +func Run(args []string) (string, error) { + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func RunAll(list CmdsList) error { + // Run each installation command + for _, args := range list { + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(args, " "), output, err) + } + fmt.Printf("%s: %s\n", style.Green.Render("Running"), strings.Join(args, " ")) + } + return nil +} + +// FindPathToFile finds path(s) to a file in a directory and its subdirectories using parallel processing +func FindPathToFile(startDir string, targetFileName string) []string { + var dirPaths []string + var mu sync.Mutex + var wg sync.WaitGroup + + fileChan := make(chan string) + + // Worker function to process files + worker := func() { + defer wg.Done() + for path := range fileChan { + info, err := os.Stat(path) + if err != nil { + continue + } + if !info.IsDir() && info.Name() == targetFileName { + dirPath := filepath.Dir(path) + mu.Lock() + dirPaths = append(dirPaths, dirPath) + mu.Unlock() + } + } + } + + // Start worker goroutines + numWorkers := 4 // Adjust the number of workers based on your system's capabilities + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go worker() + } + + // Walk the directory tree and send file paths to the channel + err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + // Ignore permission errors + if os.IsPermission(err) { + return nil + } + return err + } + fileChan <- path + return nil + }) + + close(fileChan) + wg.Wait() + + if err != nil { + panic(err) + } + + return dirPaths +} diff --git a/internal/system/service.go b/internal/system/service.go new file mode 100644 index 00000000..7a573f02 --- /dev/null +++ b/internal/system/service.go @@ -0,0 +1,16 @@ +package system + +type Interface interface { + IsInstalled() bool + IsRunning() bool + IsService() bool + SetNetwork(network string) error + Install() error + Update() error + Uninstall() error + Start() error + Stop() error + Restart() error + UpdateService(dataDirectoryPath string) error + EnsureService() error +} From 8414f718e8a59a11b52fab22f3debc8b0042b821 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Wed, 11 Dec 2024 19:44:55 -0500 Subject: [PATCH 18/23] feat(node): charmbracelet logger --- cmd/configure/configure.go | 10 +- cmd/node/debug.go | 39 ++++- cmd/node/install.go | 38 ++++- cmd/node/node.go | 27 ++-- cmd/node/start.go | 27 +++- cmd/node/stop.go | 23 ++- cmd/node/uninstall.go | 31 +++- cmd/node/upgrade.go | 10 +- internal/algod/algod.go | 31 +--- internal/algod/fallback/algod.go | 34 ++-- internal/algod/linux/linux.go | 15 +- internal/algod/mac/mac.go | 260 ++++++++++++++----------------- internal/algod/msgs/errors.go | 5 + internal/system/cmds.go | 15 +- main.go | 13 ++ ui/app/app_test.go | 4 +- 16 files changed, 331 insertions(+), 251 deletions(-) create mode 100644 internal/algod/msgs/errors.go diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index 5b5d6336..3669e7b6 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -16,11 +16,11 @@ import ( ) var Cmd = &cobra.Command{ - Use: "configure", - Short: "Configure Algod", - Long: "Configure Algod settings", - SilenceUsage: true, - PersistentPreRunE: node.NeedsToBeStopped, + Use: "configure", + Short: "Configure Algod", + Long: "Configure Algod settings", + SilenceUsage: true, + PersistentPreRun: node.NeedsToBeStopped, //RunE: func(cmd *cobra.Command, args []string) error { // return configureNode() //}, diff --git a/cmd/node/debug.go b/cmd/node/debug.go index 80f36409..eeb77754 100644 --- a/cmd/node/debug.go +++ b/cmd/node/debug.go @@ -1,25 +1,54 @@ package node import ( + "encoding/json" "fmt" "github.com/algorandfoundation/algorun-tui/internal/algod" "github.com/algorandfoundation/algorun-tui/internal/algod/utils" "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" + "os/exec" ) +type DebugInfo struct { + InPath bool `json:"inPath"` + IsRunning bool `json:"isRunning"` + IsService bool `json:"isService"` + IsInstalled bool `json:"isInstalled"` + Algod string `json:"algod"` + Data []string `json:"data"` +} + var debugCmd = &cobra.Command{ Use: "debug", Short: "Display debug information for developers", Long: "Prints debug data to be copy and pasted to a bug report.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { + log.Info("Collecting debug information...") + + // Warn user for prompt + log.Warn(style.Yellow.Render(SudoWarningMsg)) + paths := utils.GetKnownDataPaths() - fmt.Printf("Algod in PATH: %v\n", system.CmdExists("algod")) - fmt.Printf("Algod is installed: %v\n", algod.IsInstalled()) - fmt.Printf("Algod is running: %v\n", algod.IsRunning()) - fmt.Printf("Algod is service: %v\n", algod.IsService()) - fmt.Printf("Algod paths: %+v\n", paths) + path, _ := exec.LookPath("algod") + info := DebugInfo{ + InPath: system.CmdExists("algod"), + IsRunning: algod.IsRunning(), + IsService: algod.IsService(), + IsInstalled: algod.IsInstalled(), + Algod: path, + Data: paths, + } + data, err := json.MarshalIndent(info, "", " ") + if err != nil { + return err + } + + log.Info(style.Blue.Render("Copy and paste the following to a bug report:")) + fmt.Println(style.Bold(string(data))) return nil }, } diff --git a/cmd/node/install.go b/cmd/node/install.go index 051631c1..e66c0217 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -1,23 +1,45 @@ package node import ( - "fmt" "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" + "os" ) +const InstallMsg = "Installing Algorand" const InstallExistsMsg = "algod is already installed" var installCmd = &cobra.Command{ Use: "install", - Short: "Install Algorand node (Algod)", - Long: "Install Algorand node (Algod) and other binaries on your system", + Short: "Install the algorand daemon", + Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("Install the algorand daemon on your local machine"), SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("Checking if Algod is installed...") - if algod.IsInstalled() { - return fmt.Errorf(InstallExistsMsg) + Run: func(cmd *cobra.Command, args []string) { + // TODO: yes flag + + // TODO: get expected version + log.Info(style.Green.Render(InstallMsg + " vX.X.X")) + // Warn user for prompt + log.Warn(style.Yellow.Render(SudoWarningMsg)) + + // TODO: compare expected version to existing version + if algod.IsInstalled() && !force { + log.Error(InstallExistsMsg) + os.Exit(1) + } + + // Run the installation + err := algod.Install() + if err != nil { + log.Error(err) + os.Exit(1) } - return algod.Install() + log.Info(style.Green.Render("Algorand installed successfully 🎉")) }, } + +func init() { + installCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully install the node")) +} diff --git a/cmd/node/node.go b/cmd/node/node.go index 4741c2ea..96eb36e3 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -2,24 +2,29 @@ package node import ( "errors" - "fmt" "github.com/algorandfoundation/algorun-tui/internal/algod" "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" "os" "runtime" ) +const SudoWarningMsg = "(You may be prompted for your password)" const PermissionErrorMsg = "this command must be run with super-user privileges (sudo)" const NotInstalledErrorMsg = "algod is not installed. please run the *node install* command" const RunningErrorMsg = "algod is running, please run the *node stop* command" const NotRunningErrorMsg = "algod is not running" +var ( + force bool = false +) var Cmd = &cobra.Command{ Use: "node", Short: "Node Management", Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Check that we are calling with sudo on linux if os.Geteuid() != 0 && runtime.GOOS == "linux" { return errors.New(PermissionErrorMsg) @@ -28,24 +33,28 @@ var Cmd = &cobra.Command{ }, } -func NeedsToBeRunning(cmd *cobra.Command, args []string) error { +func NeedsToBeRunning(cmd *cobra.Command, args []string) { + if force { + return + } if !algod.IsInstalled() { - return fmt.Errorf(NotInstalledErrorMsg) + log.Fatal(NotInstalledErrorMsg) } if !algod.IsRunning() { - return fmt.Errorf(NotRunningErrorMsg) + log.Fatal(NotRunningErrorMsg) } - return nil } -func NeedsToBeStopped(cmd *cobra.Command, args []string) error { +func NeedsToBeStopped(cmd *cobra.Command, args []string) { + if force { + return + } if !algod.IsInstalled() { - return fmt.Errorf(NotInstalledErrorMsg) + log.Fatal(NotInstalledErrorMsg) } if algod.IsRunning() { - return fmt.Errorf(RunningErrorMsg) + log.Fatal(RunningErrorMsg) } - return nil } func init() { diff --git a/cmd/node/start.go b/cmd/node/start.go index 0691e615..43cfb444 100644 --- a/cmd/node/start.go +++ b/cmd/node/start.go @@ -2,16 +2,29 @@ package node import ( "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" ) var startCmd = &cobra.Command{ - Use: "start", - Short: "Start Algod", - Long: "Start Algod on your system (the one on your PATH).", - SilenceUsage: true, - PersistentPreRunE: NeedsToBeStopped, - RunE: func(cmd *cobra.Command, args []string) error { - return algod.Start() + Use: "start", + Short: "Start Algod", + Long: "Start Algod on your system (the one on your PATH).", + SilenceUsage: true, + PersistentPreRun: NeedsToBeStopped, + Run: func(cmd *cobra.Command, args []string) { + log.Info(style.Green.Render("Starting Algod 🚀")) + // Warn user for prompt + log.Warn(style.Yellow.Render(SudoWarningMsg)) + err := algod.Start() + if err != nil { + log.Fatal(err) + } + log.Info(style.Green.Render("Algorand started successfully 🎉")) }, } + +func init() { + startCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully start the node")) +} diff --git a/cmd/node/stop.go b/cmd/node/stop.go index 02a6a5c0..878c3c37 100644 --- a/cmd/node/stop.go +++ b/cmd/node/stop.go @@ -3,6 +3,8 @@ package node import ( "fmt" "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "time" "github.com/spf13/cobra" @@ -13,13 +15,16 @@ const StopSuccessMsg = "Algod stopped successfully" const StopFailureMsg = "failed to stop Algod" var stopCmd = &cobra.Command{ - Use: "stop", - Short: "Stop Algod", - Long: "Stop the Algod process on your system.", - SilenceUsage: true, - PersistentPreRunE: NeedsToBeRunning, + Use: "stop", + Short: "Stop Algod", + Long: "Stop the Algod process on your system.", + SilenceUsage: true, + PersistentPreRun: NeedsToBeRunning, RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("Stopping Algod...") + log.Info(style.Green.Render("Stopping Algod 😢")) + // Warn user for prompt + log.Warn(style.Yellow.Render(SudoWarningMsg)) + err := algod.Stop() if err != nil { return fmt.Errorf(StopFailureMsg) @@ -30,7 +35,11 @@ var stopCmd = &cobra.Command{ return fmt.Errorf(StopFailureMsg) } - fmt.Println(StopSuccessMsg) + log.Info(style.Green.Render("Algorand stopped successfully 🎉")) return nil }, } + +func init() { + stopCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully stop the node")) +} diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go index 9a60b86f..350c5ee3 100644 --- a/cmd/node/uninstall.go +++ b/cmd/node/uninstall.go @@ -2,16 +2,33 @@ package node import ( "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" ) +const UninstallWarningMsg = "(You may be prompted for your password to uninstall)" + 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.", - SilenceUsage: true, - PersistentPreRunE: NeedsToBeStopped, - RunE: func(cmd *cobra.Command, args []string) error { - return algod.Uninstall() + Use: "uninstall", + Short: "Uninstall Algorand node (Algod)", + Long: "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.", + SilenceUsage: true, + PersistentPreRun: NeedsToBeStopped, + Run: func(cmd *cobra.Command, args []string) { + if force { + log.Warn(style.Red.Render("Uninstalling Algorand (forcefully)")) + } + // Warn user for prompt + log.Warn(style.Yellow.Render(UninstallWarningMsg)) + + err := algod.Uninstall(force) + if err != nil { + log.Fatal(err) + } }, } + +func init() { + uninstallCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully uninstall the node")) +} diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go index ee675686..5b8cce5e 100644 --- a/cmd/node/upgrade.go +++ b/cmd/node/upgrade.go @@ -6,11 +6,11 @@ import ( ) var upgradeCmd = &cobra.Command{ - Use: "upgrade", - Short: "Upgrade Algod", - Long: "Upgrade Algod (if installed with package manager).", - SilenceUsage: true, - PersistentPreRunE: NeedsToBeStopped, + Use: "upgrade", + Short: "Upgrade Algod", + Long: "Upgrade Algod (if installed with package manager).", + SilenceUsage: true, + PersistentPreRun: NeedsToBeStopped, RunE: func(cmd *cobra.Command, args []string) error { // TODO: Check Version from S3 against the local binary return algod.Update() diff --git a/internal/algod/algod.go b/internal/algod/algod.go index e19737f6..34ad1537 100644 --- a/internal/algod/algod.go +++ b/internal/algod/algod.go @@ -5,7 +5,6 @@ import ( "github.com/algorandfoundation/algorun-tui/internal/algod/linux" "github.com/algorandfoundation/algorun-tui/internal/algod/mac" "github.com/algorandfoundation/algorun-tui/internal/system" - "os/exec" "runtime" ) @@ -14,20 +13,7 @@ const UnsupportedOSError = "unsupported operating system" // IsInstalled checks if the Algod software is installed on the system // by verifying its presence and service setup. func IsInstalled() bool { - //If algod is not in the path - if !system.CmdExists("algod") { - return false - } - // If the service is listed - switch runtime.GOOS { - case "linux": - return linux.IsService() - case "darwin": - return mac.IsService() - default: - fmt.Println("Unsupported operating system.") - return false - } + return system.CmdExists("algod") } // IsRunning checks if the algod is currently running on the host operating system. @@ -36,9 +22,8 @@ func IsInstalled() bool { func IsRunning() bool { switch runtime.GOOS { case "linux", "darwin": - fmt.Println("Checking if algod is running...") - err := exec.Command("pgrep", "algod").Run() - return err == nil + return system.IsCmdRunning("algod") + default: return false } @@ -83,7 +68,7 @@ func Update() error { case "linux": return linux.Upgrade() case "darwin": - return mac.Upgrade() + return mac.Upgrade(false) default: return fmt.Errorf(UnsupportedOSError) } @@ -91,12 +76,12 @@ func Update() error { // Uninstall removes the Algorand software from the system based // on the host operating system using appropriate methods. -func Uninstall() error { +func Uninstall(force bool) error { switch runtime.GOOS { case "linux": return linux.Uninstall() case "darwin": - return mac.Uninstall() + return mac.Uninstall(force) default: return fmt.Errorf(UnsupportedOSError) } @@ -134,7 +119,7 @@ func Start() error { case "linux": return linux.Start() case "darwin": - return mac.Start() + return mac.Start(false) default: // Unsupported OS return fmt.Errorf(UnsupportedOSError) } @@ -147,7 +132,7 @@ func Stop() error { case "linux": return linux.Stop() case "darwin": - return mac.Stop() + return mac.Stop(false) default: return fmt.Errorf(UnsupportedOSError) } diff --git a/internal/algod/fallback/algod.go b/internal/algod/fallback/algod.go index e0ff3dc7..c83c4454 100644 --- a/internal/algod/fallback/algod.go +++ b/internal/algod/fallback/algod.go @@ -3,16 +3,17 @@ package fallback import ( "errors" "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod/msgs" "github.com/algorandfoundation/algorun-tui/internal/algod/utils" "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/charmbracelet/log" "os" "os/exec" "syscall" ) -func isRunning() (bool, error) { - return false, errors.New("not implemented") -} +// Install executes a series of commands to set up the Algorand node and development tools on a Unix environment. +// TODO: Allow for changing of the paths func Install() error { return system.RunAll(system.CmdsList{ {"mkdir", "~/node"}, @@ -21,28 +22,28 @@ func Install() error { {"chmod", "744", "update.sh"}, {"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"}, }) + } func Start() error { - // Algod is not available as a systemd service, start it directly - fmt.Println("Starting algod directly...") + path, err := exec.LookPath("algod") + log.Debug("Starting algod", "path", path) // Check if ALGORAND_DATA environment variable is set - fmt.Println("Checking if ALGORAND_DATA env var is set...") + log.Info("Checking if ALGORAND_DATA env var is set...") algorandData := os.Getenv("ALGORAND_DATA") if !utils.IsDataDir(algorandData) { - fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.") - os.Exit(1) + return errors.New(msgs.InvalidDataDirectory) } - fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData) + log.Info("ALGORAND_DATA env var set to valid directory: " + algorandData) cmd := exec.Command("algod") cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } - err := cmd.Start() + err = cmd.Start() if err != nil { return fmt.Errorf("Failed to start algod: %v\n", err) } @@ -50,30 +51,29 @@ func Start() error { } func Stop() error { - // Algod is not available as a systemd service, stop it directly - fmt.Println("Stopping algod directly...") + log.Debug("Manually shutting down algod") // Find the process ID of algod pid, err := findAlgodPID() if err != nil { - return fmt.Errorf("Failed to find algod process: %v\n", err) + return err } // Send SIGTERM to the process process, err := os.FindProcess(pid) if err != nil { - return fmt.Errorf("Failed to find process with PID %d: %v\n", pid, err) + return err } err = process.Signal(syscall.SIGTERM) if err != nil { - return fmt.Errorf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err) + return err } - fmt.Println("Sent SIGTERM to algod process.") return nil } func findAlgodPID() (int, error) { + log.Debug("Scanning for algod process") cmd := exec.Command("pgrep", "algod") output, err := cmd.Output() if err != nil { @@ -83,7 +83,7 @@ func findAlgodPID() (int, error) { var pid int _, err = fmt.Sscanf(string(output), "%d", &pid) if err != nil { - return 0, fmt.Errorf("failed to parse PID: %v", err) + return 0, err } return pid, nil diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go index acd61804..83ceed9c 100644 --- a/internal/algod/linux/linux.go +++ b/internal/algod/linux/linux.go @@ -5,7 +5,8 @@ import ( "fmt" "github.com/algorandfoundation/algorun-tui/internal/algod/fallback" "github.com/algorandfoundation/algorun-tui/internal/system" - "log" + "github.com/charmbracelet/log" + "os" "os/exec" "strings" @@ -22,10 +23,10 @@ type Algod struct { // Install installs Algorand development tools or node software depending on the package manager. func Install() error { - log.Println("Installing Algod on Linux") + log.Info("Installing Algod on Linux") // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager if system.CmdExists("apt-get") { // On some Debian systems we use apt-get - log.Printf("Installing with apt-get") + log.Info("Installing with apt-get") return system.RunAll(system.CmdsList{ {"apt-get", "update"}, {"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"}, @@ -58,18 +59,18 @@ func Install() error { // Uninstall removes the Algorand software using a supported package manager or clears related system files if necessary. // Returns an error if a supported package manager is not found or if any command fails during execution. func Uninstall() error { - fmt.Println("Uninstalling Algorand") + log.Info("Uninstalling Algorand") var unInstallCmds system.CmdsList // On Ubuntu and Debian there's the apt package manager if system.CmdExists("apt-get") { - fmt.Println("Using apt-get package manager") + log.Info("Using apt-get package manager") unInstallCmds = [][]string{ {"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"}, } } // On Fedora and CentOs8 there's the dnf package manager if system.CmdExists("dnf") { - fmt.Println("Using dnf package manager") + log.Info("Using dnf package manager") unInstallCmds = [][]string{ {"dnf", "remove", "algorand-devtools", "algorand", "-y"}, } @@ -192,7 +193,7 @@ ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}` os.Exit(1) } - fmt.Println("Algorand service file updated successfully.") + log.Info("Algorand service file updated successfully.") return nil } diff --git a/internal/algod/mac/mac.go b/internal/algod/mac/mac.go index f26eb7cc..8afeeeb7 100644 --- a/internal/algod/mac/mac.go +++ b/internal/algod/mac/mac.go @@ -4,7 +4,9 @@ import ( "bytes" "errors" "fmt" + "github.com/algorandfoundation/algorun-tui/internal/algod/utils" "github.com/algorandfoundation/algorun-tui/internal/system" + "github.com/charmbracelet/log" "github.com/spf13/cobra" "io" "net/http" @@ -15,11 +17,14 @@ import ( "text/template" ) +const MustBeServiceMsg = "service must be installed to be able to manage it" +const HomeBrewNotFoundMsg = "homebrew is not installed. please install Homebrew and try again" + // IsService 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 IsService() bool { - _, err := system.Run([]string{"launchctl", "list", "com.algorand.algod"}) + _, err := system.Run([]string{"sudo", "launchctl", "list", "com.algorand.algod"}) return err == nil } @@ -27,11 +32,11 @@ func IsService() bool { // configures necessary directories, and ensures it // runs as a background service. func Install() error { - fmt.Println("Installing Algod on macOS...") + log.Info("Installing Algod on macOS...") // Homebrew is our package manager of choice if !system.CmdExists("brew") { - return fmt.Errorf("could not find Homebrew installed. Please install Homebrew and try again") + return errors.New(HomeBrewNotFoundMsg) } err := system.RunAll(system.CmdsList{ @@ -44,81 +49,105 @@ func Install() error { } // Handle data directory and genesis.json file - handleDataDirMac() + err = handleDataDirMac() + if err != nil { + return err + } path, err := os.Executable() if err != nil { return err } + // Create and load the launchd service - _, err = system.Run([]string{"sudo", path, "configure", "service"}) + // TODO: find a clever way to avoid this or make sudo persist for the second call + err = system.RunAll(system.CmdsList{{"sudo", path, "configure", "service"}}) if err != nil { - return fmt.Errorf("failed to create and load launchd service: %v\n", err) + return err } - // Ensure Homebrew bin directory is in the PATH - // So that brew installed algorand binaries can be found - ensureHomebrewPathInEnv() - if !IsService() { - return fmt.Errorf("algod unexpectedly NOT in path. Installation failed") + return fmt.Errorf("algod is not a service") } - fmt.Println(`Installed Algorand (Algod) with Homebrew. -Algod is running in the background as a system-level service. - `) + log.Info("Installed Algorand (Algod) with Homebrew ") return nil } // Uninstall removes the Algorand application from the system using Homebrew if it is installed. -func Uninstall() error { - if !system.CmdExists("brew") { +func Uninstall(force bool) error { + if force { + if system.IsCmdRunning("algod") { + err := Stop(force) + if err != nil { + return err + } + } + } + + cmds := system.CmdsList{} + if IsService() { + cmds = append(cmds, []string{"sudo", "launchctl", "unload", "/Library/LaunchDaemons/com.algorand.algod.plist"}) + } + + if !system.CmdExists("brew") && !force { return errors.New("homebrew is not installed") + } else { + cmds = append(cmds, []string{"brew", "uninstall", "algorand"}) + } + + if force { + cmds = append(cmds, []string{"sudo", "rm", "-rf", strings.Join(utils.GetKnownDataPaths(), " ")}) + cmds = append(cmds, []string{"sudo", "rm", "-rf", "/Library/LaunchDaemons/com.algorand.algod.plist"}) } - return exec.Command("brew", "uninstall", "algorand").Run() + + return system.RunAll(cmds) } // Upgrade updates the installed Algorand package using Homebrew if it's available and properly configured. -func Upgrade() error { +func Upgrade(force bool) error { if !system.CmdExists("brew") { return errors.New("homebrew is not installed") } - // Check if algorand is installed with Homebrew - checkCmdArgs := system.CmdsList{{"brew", "--prefix", "algorand", "--installed"}} - if system.IsSudo() { - checkCmdArgs = checkCmdArgs.Su(os.Getenv("SUDO_USER")) - } - err := system.RunAll(checkCmdArgs) - if err != nil { - return err - } - // Upgrade algorand - upgradeCmdArgs := system.CmdsList{{"brew", "upgrade", "algorand", "--formula"}} - if system.IsSudo() { - upgradeCmdArgs = upgradeCmdArgs.Su(os.Getenv("SUDO_USER")) - } - return system.RunAll(upgradeCmdArgs) + + return system.RunAll(system.CmdsList{ + {"brew", "--prefix", "algorand", "--installed"}, + {"brew", "upgrade", "algorand", "--formula"}, + }) } // Start algorand with launchd -func Start() error { - return exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist").Run() +func Start(force bool) error { + log.Debug("Attempting to start algorand with launchd") + //if !IsService() && !force { + // return fmt.Errorf(MustBeServiceMsg) + //} + return system.RunAll(system.CmdsList{ + {"sudo", "launchctl", "start", "com.algorand.algod"}, + }) } // Stop shuts down the Algorand algod system process using the launchctl bootout command. // Returns an error if the operation fails. -func Stop() error { - return exec.Command("launchctl", "bootout", "system/com.algorand.algod").Run() +func Stop(force bool) error { + if !IsService() && !force { + return fmt.Errorf(MustBeServiceMsg) + } + + return system.RunAll(system.CmdsList{ + {"sudo", "launchctl", "stop", "com.algorand.algod"}, + }) } // UpdateService updates the Algorand launchd service with // a new data directory path and reloads the service configuration. +// TODO: Deduplicate this method, redundant version of EnsureService. func UpdateService(dataDirectoryPath string) error { algodPath, err := exec.LookPath("algod") if err != nil { - fmt.Printf("Failed to find algod binary: %v\n", err) + log.Info("Failed to find algod binary: %v\n", err) os.Exit(1) } @@ -138,8 +167,6 @@ func UpdateService(dataDirectoryPath string) error { </array> <key>RunAtLoad</key> <true/> - <key>KeepAlive</key> - <true/> <key>StandardOutPath</key> <string>/tmp/algod.out</string> <key>StandardErrorPath</key> @@ -156,21 +183,21 @@ func UpdateService(dataDirectoryPath string) error { // Parse and execute the template tmpl, err := template.New("override").Parse(overwriteTemplate) if err != nil { - fmt.Printf("Failed to parse template: %v\n", err) + log.Info("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) + log.Info("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) + log.Info("Failed to write override file: %v\n", err) os.Exit(1) } @@ -179,7 +206,7 @@ func UpdateService(dataDirectoryPath string) error { err = cmd.Run() if err != nil { if !strings.Contains(err.Error(), "No such process") { - fmt.Printf("Failed to bootout launchd service: %v\n", err) + log.Info("Failed to bootout launchd service: %v\n", err) os.Exit(1) } } @@ -188,7 +215,7 @@ func UpdateService(dataDirectoryPath string) error { cmd = exec.Command("launchctl", "load", overwriteFilePath) err = cmd.Run() if err != nil { - fmt.Printf("Failed to load launchd service: %v\n", err) + log.Info("Failed to load launchd service: %v\n", err) os.Exit(1) } @@ -196,57 +223,55 @@ func UpdateService(dataDirectoryPath string) error { return nil } -func handleDataDirMac() { +// TODO move to configure as a generic +func handleDataDirMac() error { // 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) + return err } // Check if genesis.json file exists in ~/.algorand + // TODO: replace with algocfg or goal templates 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() + _, err := os.Stat(genesisFilePath) + if !os.IsNotExist(err) { + return nil + } - // 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() + log.Info("Downloading mainnet genesis.json file to ~/.algorand/genesis.json") - // 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) - } + // 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 { + return err + } + defer resp.Body.Close() - fmt.Println("mainnet genesis.json file downloaded successfully.") + // Create the file + out, err := os.Create(genesisFilePath) + if err != nil { + return err + } + defer out.Close() + + // Write the content to the file + _, err = io.Copy(out, resp.Body) + if err != nil { + return err } + log.Info("mainnet genesis.json file downloaded successfully.") + return nil } func EnsureService() error { - // Get the prefix path for Algorand - cmd := exec.Command("brew", "--prefix", "algorand") - algorandPrefix, err := cmd.Output() + log.Debug("Ensuring Algorand service is running") + path, err := exec.LookPath("algod") if err != nil { - fmt.Printf("Failed to get Algorand prefix: %v\n", err) - cobra.CheckErr(err) + log.Error("algod does not exist in path") + return err } - algorandPrefixPath := strings.TrimSpace(string(algorandPrefix)) - // Define the launchd plist content plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> @@ -256,93 +281,36 @@ func EnsureService() error { <string>com.algorand.algod</string> <key>ProgramArguments</key> <array> - <string>%s/bin/algod</string> + <string>%s</string> <string>-d</string> <string>%s/.algorand</string> </array> <key>RunAtLoad</key> <true/> - <key>KeepAlive</key> - <true/> + <key>Debug</key> + <true/> <key>StandardOutPath</key> <string>/tmp/algod.out</string> <key>StandardErrorPath</key> <string>/tmp/algod.err</string> </dict> -</plist>`, algorandPrefixPath, os.Getenv("HOME")) +</plist>`, path, 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) + log.Info("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) + log.Info("Failed to write plist file: %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.") - - return nil -} - -// 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) - } - } + return system.RunAll(system.CmdsList{ + {"launchctl", "load", plistPath}, + {"launchctl", "list", "com.algorand.algod"}, + }) } diff --git a/internal/algod/msgs/errors.go b/internal/algod/msgs/errors.go new file mode 100644 index 00000000..d68b0c52 --- /dev/null +++ b/internal/algod/msgs/errors.go @@ -0,0 +1,5 @@ +package msgs + +const ( + InvalidDataDirectory = "algorand data directory is invalid" +) diff --git a/internal/system/cmds.go b/internal/system/cmds.go index 02f5c633..45b9ab48 100644 --- a/internal/system/cmds.go +++ b/internal/system/cmds.go @@ -3,6 +3,7 @@ package system import ( "fmt" "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "os" "os/exec" "path/filepath" @@ -10,10 +11,17 @@ import ( "sync" ) +const CmdFailedErrorMsg = "command failed: %s output: %s error: %v" + func IsSudo() bool { return os.Geteuid() == 0 } +func IsCmdRunning(name string) bool { + err := exec.Command("pgrep", name).Run() + return err == nil +} + // CmdExists checks that a bash cli/cmd tool exists func CmdExists(tool string) bool { _, err := exec.LookPath(tool) @@ -43,9 +51,10 @@ func RunAll(list CmdsList) error { cmd := exec.Command(args[0], args[1:]...) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(args, " "), output, err) + log.Error(fmt.Sprintf("%s: %s", style.Red.Render("Failed"), strings.Join(args, " "))) + return fmt.Errorf(CmdFailedErrorMsg, strings.Join(args, " "), output, err) } - fmt.Printf("%s: %s\n", style.Green.Render("Running"), strings.Join(args, " ")) + log.Debug(fmt.Sprintf("%s: %s", style.Green.Render("Running"), strings.Join(args, " "))) } return nil } @@ -85,7 +94,7 @@ func FindPathToFile(startDir string, targetFileName string) []string { // Walk the directory tree and send file paths to the channel err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error { if err != nil { - // Ignore permission errors + // Ignore permission msgs if os.IsPermission(err) { return nil } diff --git a/main.go b/main.go index f282015a..5dc55870 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,21 @@ package main import ( "github.com/algorandfoundation/algorun-tui/cmd" + "github.com/charmbracelet/log" + "os" ) +func init() { + // Log as JSON instead of the default ASCII formatter. + //log.SetFormatter(log.JSONFormatter) + + // Output to stdout instead of the default stderr + // Can be any io.Writer, see below for File example + log.SetOutput(os.Stdout) + + // Only log the warning severity or above. + log.SetLevel(log.DebugLevel) +} func main() { err := cmd.Execute() if err != nil { diff --git a/ui/app/app_test.go b/ui/app/app_test.go index 311ffdb4..90ce2deb 100644 --- a/ui/app/app_test.go +++ b/ui/app/app_test.go @@ -46,7 +46,7 @@ func Test_EmitDeleteKey(t *testing.T) { t.Error("Expected ABC") } if evt.Err != nil { - t.Error("Expected no errors") + t.Error("Expected no msgs") } client = test.GetClient(true) @@ -60,7 +60,7 @@ func Test_EmitDeleteKey(t *testing.T) { t.Error("Expected no response") } if evt.Err == nil { - t.Error("Expected errors") + t.Error("Expected msgs") } } From 965404b73e652d4ddfc45aeb76c51cba34f9ffa7 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Wed, 11 Dec 2024 21:23:15 -0500 Subject: [PATCH 19/23] test(node): update playbook and workflow --- .github/workflows/node_test.yaml | 20 ++++++++------------ playbook.yaml | 2 -- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml index 52421251..2a3ebebf 100644 --- a/.github/workflows/node_test.yaml +++ b/.github/workflows/node_test.yaml @@ -24,15 +24,13 @@ jobs: - name: Run Ubuntu commands run: | go build . - sudo ./algorun-tui node install - sudo ./algorun-tui node start + ./algorun-tui node install systemctl status algorand.service export TOKEN=$(cat /var/lib/algorand/algod.admin.token) curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" - sudo ./algorun-tui node stop - sudo ./algorun-tui node upgrade - # sudo ./algorun-tui node configure - sudo ./algorun-tui node uninstall + ./algorun-tui node stop + ./algorun-tui node upgrade + ./algorun-tui node uninstall macos: runs-on: macos-latest @@ -46,13 +44,11 @@ jobs: - name: Run MacOs commands run: | go build . - sudo ./algorun-tui node install - sudo ./algorun-tui node start + ./algorun-tui node install sudo launchctl print system/com.algorand.algod sleep 5 export TOKEN=$(cat ~/.algorand/algod.admin.token) curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" - sudo ./algorun-tui node stop - sudo ./algorun-tui node upgrade - # sudo ./algorun-tui node configure - sudo ./algorun-tui node uninstall + ./algorun-tui node stop + ./algorun-tui node upgrade + ./algorun-tui node uninstall diff --git a/playbook.yaml b/playbook.yaml index db717bec..7510210f 100644 --- a/playbook.yaml +++ b/playbook.yaml @@ -11,8 +11,6 @@ when: not binpath.stat.exists - name: Run installer command: algorun node install - - name: Run installer twice - command: algorun node install - name: Run upgrade command: algorun node upgrade - name: Run stop From 5561b8cc26747739948d8ba1d1db3a35f03c5310 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Wed, 11 Dec 2024 21:43:13 -0500 Subject: [PATCH 20/23] chore(node): call sudo on demand --- cmd/node/install.go | 10 +++++ cmd/node/node.go | 11 ----- cmd/node/upgrade.go | 24 ++++++++++- internal/algod/linux/linux.go | 75 ++++++++++++++++++++++------------- playbook.yaml | 2 + 5 files changed, 82 insertions(+), 40 deletions(-) diff --git a/cmd/node/install.go b/cmd/node/install.go index e66c0217..61a68bf1 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -36,6 +36,16 @@ var installCmd = &cobra.Command{ log.Error(err) os.Exit(1) } + + // If it's not running, start the daemon (can happen) + if !algod.IsRunning() { + err = algod.Start() + if err != nil { + log.Error(err) + os.Exit(1) + } + } + log.Info(style.Green.Render("Algorand installed successfully 🎉")) }, } diff --git a/cmd/node/node.go b/cmd/node/node.go index 96eb36e3..1fca2ecd 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -1,13 +1,10 @@ package node import ( - "errors" "github.com/algorandfoundation/algorun-tui/internal/algod" "github.com/algorandfoundation/algorun-tui/ui/style" "github.com/charmbracelet/log" "github.com/spf13/cobra" - "os" - "runtime" ) const SudoWarningMsg = "(You may be prompted for your password)" @@ -23,14 +20,6 @@ var Cmd = &cobra.Command{ Use: "node", Short: "Node Management", Long: style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"), - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - - // Check that we are calling with sudo on linux - if os.Geteuid() != 0 && runtime.GOOS == "linux" { - return errors.New(PermissionErrorMsg) - } - return nil - }, } func NeedsToBeRunning(cmd *cobra.Command, args []string) { diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go index 5b8cce5e..139d232a 100644 --- a/cmd/node/upgrade.go +++ b/cmd/node/upgrade.go @@ -2,17 +2,37 @@ package node import ( "github.com/algorandfoundation/algorun-tui/internal/algod" + "github.com/algorandfoundation/algorun-tui/ui/style" + "github.com/charmbracelet/log" "github.com/spf13/cobra" + "os" ) +const UpgradeMsg = "Upgrading Algod" + var upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade Algod", Long: "Upgrade Algod (if installed with package manager).", SilenceUsage: true, PersistentPreRun: NeedsToBeStopped, - RunE: func(cmd *cobra.Command, args []string) error { + Run: func(cmd *cobra.Command, args []string) { + // TODO: get expected version and check if update is required + log.Info(style.Green.Render(UpgradeMsg + " vX.X.X")) + // Warn user for prompt + log.Warn(style.Yellow.Render(SudoWarningMsg)) // TODO: Check Version from S3 against the local binary - return algod.Update() + err := algod.Update() + if err != nil { + log.Error(err) + } + // If it's not running, start the daemon (can happen) + if !algod.IsRunning() { + err = algod.Start() + if err != nil { + log.Error(err) + os.Exit(1) + } + } }, } diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go index 83ceed9c..4820a42b 100644 --- a/internal/algod/linux/linux.go +++ b/internal/algod/linux/linux.go @@ -6,9 +6,9 @@ import ( "github.com/algorandfoundation/algorun-tui/internal/algod/fallback" "github.com/algorandfoundation/algorun-tui/internal/system" "github.com/charmbracelet/log" - "os" "os/exec" + "runtime" "strings" "text/template" ) @@ -21,34 +21,55 @@ type Algod struct { DataDirectoryPath string } +// InstallRequirements generates installation commands for "sudo" based on the detected package manager and system state. +func InstallRequirements() system.CmdsList { + var cmds system.CmdsList + if (system.CmdExists("sudo") && system.CmdExists("prep")) || os.Geteuid() != 0 { + return cmds + } + if system.CmdExists("apt-get") { + return system.CmdsList{ + {"apt-get", "update"}, + {"apt-get", "install", "-y", "sudo", "procps"}, + } + } + + if system.CmdExists("dnf") { + return system.CmdsList{ + {"dnf", "install", "-y", "sudo", "procps-ng"}, + } + } + return cmds +} + // Install installs Algorand development tools or node software depending on the package manager. func Install() error { log.Info("Installing Algod on Linux") // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager if system.CmdExists("apt-get") { // On some Debian systems we use apt-get log.Info("Installing with apt-get") - return system.RunAll(system.CmdsList{ - {"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"}, - }) + return system.RunAll(append(InstallRequirements(), system.CmdsList{ + {"sudo", "apt-get", "update"}, + {"sudo", "apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"}, + {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | sudo tee /etc/apt/trusted.gpg.d/algorand.asc"}, + {"sudo", "add-apt-repository", "-y", fmt.Sprintf("deb [arch=%s] https://releases.algorand.com/deb/ stable main", runtime.GOARCH)}, + {"sudo", "apt-get", "update"}, + {"sudo", "apt-get", "install", "-y", "algorand-devtools"}, + }...)) } if system.CmdExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager log.Printf("Installing with dnf") - return system.RunAll(system.CmdsList{ + return system.RunAll(append(InstallRequirements(), system.CmdsList{ {"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", "enable", "algorand.service"}, - {"systemctl", "start", "algorand.service"}, + {"sudo", "rpmkeys", "--import", "rpm_algorand.pub"}, + {"sudo", "dnf", "install", "-y", "dnf-command(config-manager)"}, + {"sudo", "dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"}, + {"sudo", "dnf", "install", "-y", "algorand-devtools"}, + {"sudo", "systemctl", "enable", "algorand.service"}, + {"sudo", "systemctl", "start", "algorand.service"}, {"rm", "-f", "rpm_algorand.pub"}, - }) + }...)) } @@ -65,14 +86,14 @@ func Uninstall() error { if system.CmdExists("apt-get") { log.Info("Using apt-get package manager") unInstallCmds = [][]string{ - {"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"}, + {"sudo", "apt-get", "autoremove", "algorand-devtools", "algorand", "-y"}, } } // On Fedora and CentOs8 there's the dnf package manager if system.CmdExists("dnf") { log.Info("Using dnf package manager") unInstallCmds = [][]string{ - {"dnf", "remove", "algorand-devtools", "algorand", "-y"}, + {"sudo", "dnf", "remove", "algorand-devtools", "algorand", "-y"}, } } // Error on unsupported package managers @@ -81,8 +102,8 @@ func Uninstall() error { } // Commands to clear systemd algorand.service and any other files, like the configuration override - unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "rm -rf /etc/systemd/system/algorand*"}) - unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"}) + unInstallCmds = append(unInstallCmds, []string{"sudo", "bash", "-c", "rm -rf /etc/systemd/system/algorand*"}) + unInstallCmds = append(unInstallCmds, []string{"sudo", "systemctl", "daemon-reload"}) return system.RunAll(unInstallCmds) } @@ -92,13 +113,13 @@ func Uninstall() error { func Upgrade() error { if system.CmdExists("apt-get") { return system.RunAll(system.CmdsList{ - {"apt-get", "update"}, - {"apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"}, + {"sudo", "apt-get", "update"}, + {"sudo", "apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"}, }) } if system.CmdExists("dnf") { return system.RunAll(system.CmdsList{ - {"dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"}, + {"sudo", "dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"}, }) } return fmt.Errorf("the *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade") @@ -109,21 +130,21 @@ func Upgrade() error { // Returns an error if the command fails. // TODO: Replace with D-Bus integration func Start() error { - return exec.Command("systemctl", "start", "algorand").Run() + return exec.Command("sudo", "systemctl", "start", "algorand").Run() } // Stop shuts down the Algorand algod system process on Linux using the systemctl stop command. // Returns an error if the operation fails. // TODO: Replace with D-Bus integration func Stop() error { - return exec.Command("systemctl", "stop", "algorand").Run() + return exec.Command("sudo", "systemctl", "stop", "algorand").Run() } // IsService checks if the "algorand.service" is listed as a systemd unit file on Linux. // Returns true if it exists. // TODO: Replace with D-Bus integration func IsService() bool { - out, err := system.Run([]string{"systemctl", "list-unit-files", "algorand.service"}) + out, err := system.Run([]string{"sudo", "systemctl", "list-unit-files", "algorand.service"}) if err != nil { return false } diff --git a/playbook.yaml b/playbook.yaml index 7510210f..097634f7 100644 --- a/playbook.yaml +++ b/playbook.yaml @@ -11,6 +11,8 @@ when: not binpath.stat.exists - name: Run installer command: algorun node install + - name: Run stop + command: algorun node stop - name: Run upgrade command: algorun node upgrade - name: Run stop From 652368bdb62eba5e635d823bb4229cd03b4c7021 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Thu, 12 Dec 2024 14:58:23 -0500 Subject: [PATCH 21/23] chore(node): add wait before starting --- cmd/node/install.go | 3 +++ cmd/node/upgrade.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/cmd/node/install.go b/cmd/node/install.go index 61a68bf1..e2a6d516 100644 --- a/cmd/node/install.go +++ b/cmd/node/install.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/log" "github.com/spf13/cobra" "os" + "time" ) const InstallMsg = "Installing Algorand" @@ -37,6 +38,8 @@ var installCmd = &cobra.Command{ os.Exit(1) } + time.Sleep(5 * time.Second) + // If it's not running, start the daemon (can happen) if !algod.IsRunning() { err = algod.Start() diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go index 139d232a..b464f899 100644 --- a/cmd/node/upgrade.go +++ b/cmd/node/upgrade.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/log" "github.com/spf13/cobra" "os" + "time" ) const UpgradeMsg = "Upgrading Algod" @@ -26,6 +27,9 @@ var upgradeCmd = &cobra.Command{ if err != nil { log.Error(err) } + + time.Sleep(5 * time.Second) + // If it's not running, start the daemon (can happen) if !algod.IsRunning() { err = algod.Start() From 3d76e025e0554f4b180c323be5b8416608d18dbc Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Thu, 12 Dec 2024 15:02:00 -0500 Subject: [PATCH 22/23] build: stop service before uninstalling --- .github/workflows/node_test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml index 2a3ebebf..cacf280c 100644 --- a/.github/workflows/node_test.yaml +++ b/.github/workflows/node_test.yaml @@ -30,6 +30,7 @@ jobs: curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" ./algorun-tui node stop ./algorun-tui node upgrade + ./algorun-tui node stop ./algorun-tui node uninstall macos: @@ -51,4 +52,5 @@ jobs: curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null" ./algorun-tui node stop ./algorun-tui node upgrade + ./algorun-tui node stop ./algorun-tui node uninstall From 694564ce845f3bf66a64fabf597c462890f046a2 Mon Sep 17 00:00:00 2001 From: Michael Feher <michael.feher@algorand.foundation> Date: Thu, 12 Dec 2024 15:32:43 -0500 Subject: [PATCH 23/23] chore: set patch threshold --- codecov.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yaml b/codecov.yaml index 2c7de746..eaf149d2 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -6,4 +6,4 @@ coverage: threshold: 10% patch: default: - target: 60% \ No newline at end of file + target: 10% \ No newline at end of file