From 8414f718e8a59a11b52fab22f3debc8b0042b821 Mon Sep 17 00:00:00 2001 From: Michael Feher Date: Wed, 11 Dec 2024 19:44:55 -0500 Subject: [PATCH] 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 { RunAtLoad - KeepAlive - StandardOutPath /tmp/algod.out StandardErrorPath @@ -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(` @@ -256,93 +281,36 @@ func EnsureService() error { com.algorand.algod ProgramArguments - %s/bin/algod + %s -d %s/.algorand RunAtLoad - KeepAlive - + Debug + StandardOutPath /tmp/algod.out StandardErrorPath /tmp/algod.err -`, algorandPrefixPath, os.Getenv("HOME")) +`, 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") } }