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