package node import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strings" "text/template" "source.quilibrium.com/quilibrium/monorepo/client/utils" ) // determineVersion gets the version to install from args or defaults to "latest" func determineVersion(args []string) string { if len(args) > 0 { return args[0] } return "latest" } // confirmPaths asks the user to confirm the installation and data paths func confirmPaths(installPath, dataPath string) bool { fmt.Print("Do you want to continue with these paths? [Y/n]: ") reader := bufio.NewReader(os.Stdin) response, _ := reader.ReadString('\n') response = strings.TrimSpace(strings.ToLower(response)) return response == "" || response == "y" || response == "yes" } // createNodeUser creates a dedicated user for running the node func createNodeUser() error { fmt.Fprintf(os.Stdout, "Creating dedicated user '%s' for running the node...\n", nodeUser) // Check for sudo privileges if err := utils.CheckAndRequestSudo("Creating system user requires root privileges"); err != nil { return fmt.Errorf("failed to get sudo privileges: %w", err) } var cmd *exec.Cmd if osType == "linux" { // Check if user already exists checkCmd := exec.Command("id", nodeUser) if checkCmd.Run() == nil { fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser) return nil } // Create user on Linux cmd = exec.Command("useradd", "-r", "-s", "/bin/false", "-m", "-c", "Quilibrium Node User", nodeUser) } else if osType == "darwin" { // Check if user already exists on macOS checkCmd := exec.Command("dscl", ".", "-read", "/Users/"+nodeUser) if checkCmd.Run() == nil { fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser) return nil } // Create user on macOS // Get next available user ID uidCmd := exec.Command("dscl", ".", "-list", "/Users", "UniqueID") uidOutput, err := uidCmd.Output() if err != nil { return fmt.Errorf("failed to get user IDs: %v", err) } // Find the highest UID and add 1 var maxUID int = 500 // Start with a reasonable system UID for _, line := range strings.Split(string(uidOutput), "\n") { fields := strings.Fields(line) if len(fields) >= 2 { var uid int fmt.Sscanf(fields[len(fields)-1], "%d", &uid) if uid > maxUID && uid < 65000 { // Avoid system UIDs maxUID = uid } } } nextUID := maxUID + 1 // Create the user cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to create user: %v", err) } // Set the user's properties commands := [][]string{ {"-create", "/Users/" + nodeUser, "UniqueID", fmt.Sprintf("%d", nextUID)}, {"-create", "/Users/" + nodeUser, "PrimaryGroupID", "20"}, // staff group {"-create", "/Users/" + nodeUser, "UserShell", "/bin/false"}, {"-create", "/Users/" + nodeUser, "NFSHomeDirectory", "/var/empty"}, {"-create", "/Users/" + nodeUser, "RealName", "Quilibrium Node User"}, } for _, args := range commands { cmd = exec.Command("dscl", append([]string{"."}, args...)...) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to set user property %s: %v", args[1], err) } } // Disable the user account cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser, "Password", "*") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to disable user account: %v", err) } return nil } else { return fmt.Errorf("user creation not supported on %s", osType) } // Run the command if err := cmd.Run(); err != nil { return fmt.Errorf("failed to create user: %w", err) } fmt.Fprintf(os.Stdout, "User '%s' created successfully\n", nodeUser) return nil } // setOwnership sets the ownership of directories to the node user func setOwnership() { fmt.Fprintf(os.Stdout, "Setting ownership of %s and %s to %s...\n", installPath, dataPath, nodeUser) // Change ownership of installation directory chownCmd := exec.Command("chown", "-R", nodeUser+":"+nodeUser, installPath) if err := chownCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", installPath, err) } // Change ownership of data directory chownCmd = exec.Command("chown", "-R", nodeUser+":"+nodeUser, dataPath) if err := chownCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", dataPath, err) } } // setupLogRotation creates a logrotate configuration file for the Quilibrium node func setupLogRotation() error { // Check if we need sudo privileges for creating logrotate config if err := utils.CheckAndRequestSudo("Creating logrotate configuration requires root privileges"); err != nil { return fmt.Errorf("failed to get sudo privileges: %w", err) } // Create logrotate configuration configContent := fmt.Sprintf(`%s/*.log { daily rotate 7 compress delaycompress missingok notifempty create 0640 %s %s postrotate systemctl reload quilibrium-node >/dev/null 2>&1 || true endscript }`, logPath, nodeUser, nodeUser) // Write the configuration file configPath := "/etc/logrotate.d/quilibrium-node" if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { return fmt.Errorf("failed to create logrotate configuration: %w", err) } // Create log directory with proper permissions if err := os.MkdirAll(logPath, 0750); err != nil { return fmt.Errorf("failed to create log directory: %w", err) } // Set ownership of log directory chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, logPath) if err := chownCmd.Run(); err != nil { return fmt.Errorf("failed to set log directory ownership: %w", err) } fmt.Fprintf(os.Stdout, "Created log rotation configuration at %s\n", configPath) return nil } // finishInstallation completes the installation process by making the binary executable and creating a symlink func finishInstallation(nodeBinaryPath string, version string) { setOwnership() // Make the binary executable if err := os.Chmod(nodeBinaryPath, 0755); err != nil { fmt.Fprintf(os.Stderr, "Error making binary executable: %v\n", err) return } // Check if we need sudo privileges for creating symlink in system directory if strings.HasPrefix(defaultSymlinkPath, "/usr/") || strings.HasPrefix(defaultSymlinkPath, "/bin/") || strings.HasPrefix(defaultSymlinkPath, "/sbin/") { if err := utils.CheckAndRequestSudo(fmt.Sprintf("Creating symlink at %s requires root privileges", defaultSymlinkPath)); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to get sudo privileges: %v\n", err) return } } // Create symlink using the utils package if err := utils.CreateSymlink(nodeBinaryPath, defaultSymlinkPath); err != nil { fmt.Fprintf(os.Stderr, "Error creating symlink: %v\n", err) } // Set up log rotation if err := setupLogRotation(); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to set up log rotation: %v\n", err) } // Create systemd service file if osType == "linux" { if err := createSystemdServiceFile(); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to create systemd service file: %v\n", err) } } else if osType == "darwin" { installMacOSService() } else { fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", osType) return } // Print success message printSuccessMessage(version) } // printSuccessMessage prints a success message after installation func printSuccessMessage(version string) { fmt.Fprintf(os.Stdout, "\nSuccessfully installed Quilibrium node %s\n", version) fmt.Fprintf(os.Stdout, "Installation directory: %s\n", installPath) fmt.Fprintf(os.Stdout, "Data directory: %s\n", dataPath) fmt.Fprintf(os.Stdout, "Binary symlinked to %s\n", defaultSymlinkPath) fmt.Fprintf(os.Stdout, "Log directory: %s\n", logPath) fmt.Fprintf(os.Stdout, "Environment file: /etc/default/quilibrium-node\n") fmt.Fprintf(os.Stdout, "Service file: /etc/systemd/system/quilibrium-node.service\n") fmt.Fprintf(os.Stdout, "\nTo start the node, you can run:\n") fmt.Fprintf(os.Stdout, " %s --config %s/config/config.yaml\n", defaultSymlinkPath, dataPath) fmt.Fprintf(os.Stdout, " # Or use systemd service:\n") fmt.Fprintf(os.Stdout, " sudo systemctl start quilibrium-node\n") fmt.Fprintf(os.Stdout, "\nFor more options, run:\n") fmt.Fprintf(os.Stdout, " quilibrium-node --help\n") } // createSystemdServiceFile creates the systemd service file with environment file support func createSystemdServiceFile() error { // Check if we need sudo privileges if err := utils.CheckAndRequestSudo("Creating systemd service file requires root privileges"); err != nil { return fmt.Errorf("failed to get sudo privileges: %w", err) } // Create environment file content envContent := fmt.Sprintf(`# Quilibrium Node Environment`, dataPath) // Write environment file envPath := filepath.Join(dataPath, "config", "quilibrium.env") if err := os.WriteFile(envPath, []byte(envContent), 0640); err != nil { return fmt.Errorf("failed to create environment file: %w", err) } // Set ownership of environment file chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, envPath) if err := chownCmd.Run(); err != nil { return fmt.Errorf("failed to set environment file ownership: %w", err) } // Create systemd service file content serviceContent := fmt.Sprintf(`[Unit] Description=Quilibrium Node Service After=network.target [Service] Type=simple User=quilibrium EnvironmentFile=/opt/quilibrium/config/quilibrium.env ExecStart=/usr/local/bin/quilibrium-node --config /opt/quilibrium/config Restart=on-failure RestartSec=10 LimitNOFILE=65535 [Install] WantedBy=multi-user.target `, nodeUser, defaultSymlinkPath, dataPath) // Write service file servicePath := "/etc/systemd/system/quilibrium-node.service" if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil { return fmt.Errorf("failed to create service file: %w", err) } // Reload systemd daemon reloadCmd := exec.Command("systemctl", "daemon-reload") if err := reloadCmd.Run(); err != nil { return fmt.Errorf("failed to reload systemd daemon: %w", err) } fmt.Fprintf(os.Stdout, "Created systemd service file at %s\n", servicePath) fmt.Fprintf(os.Stdout, "Created environment file at %s\n", envPath) return nil } // installMacOSService installs a launchd service on macOS func installMacOSService() { fmt.Println("Installing launchd service for Quilibrium node...") // Create plist file content plistTemplate := ` Label {{.Label}} ProgramArguments /usr/local/bin/quilibrium-node --config /opt/quilibrium/config/ EnvironmentVariables QUILIBRIUM_DATA_DIR {{.DataPath}} QUILIBRIUM_LOG_LEVEL info QUILIBRIUM_LISTEN_GRPC_MULTIADDR /ip4/127.0.0.1/tcp/8337 QUILIBRIUM_LISTEN_REST_MULTIADDR /ip4/127.0.0.1/tcp/8338 QUILIBRIUM_STATS_MULTIADDR /dns/stats.quilibrium.com/tcp/443 QUILIBRIUM_NETWORK_ID 0 QUILIBRIUM_DEBUG false QUILIBRIUM_SIGNATURE_CHECK true RunAtLoad KeepAlive StandardErrorPath {{.LogPath}}/node.err StandardOutPath {{.LogPath}}/node.log ` // Prepare template data data := struct { Label string DataPath string ServiceName string LogPath string }{ Label: fmt.Sprintf("com.quilibrium.node"), DataPath: dataPath, ServiceName: "node", LogPath: logPath, } // Parse and execute template tmpl, err := template.New("plist").Parse(plistTemplate) if err != nil { fmt.Printf("Error creating plist template: %v\n", err) return } // Determine plist file path var plistPath = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", data.Label) // Write plist file file, err := os.Create(plistPath) if err != nil { fmt.Printf("Error creating plist file: %v\n", err) return } defer file.Close() if err := tmpl.Execute(file, data); err != nil { fmt.Printf("Error writing plist file: %v\n", err) return } // Set correct permissions chownCmd := exec.Command("chown", "root:wheel", plistPath) if err := chownCmd.Run(); err != nil { fmt.Printf("Warning: Failed to change ownership of plist file: %v\n", err) } // Load the service var loadCmd = exec.Command("launchctl", "load", "-w", plistPath) if err := loadCmd.Run(); err != nil { fmt.Printf("Error loading service: %v\n", err) fmt.Println("You may need to load the service manually.") } fmt.Printf("Launchd service installed successfully as %s\n", plistPath) fmt.Println("\nTo start the service:") fmt.Printf(" sudo launchctl start %s\n", data.Label) fmt.Println("\nTo stop the service:") fmt.Printf(" sudo launchctl stop %s\n", data.Label) fmt.Println("\nTo view service logs:") fmt.Printf(" cat %s/%s.log\n", data.LogPath, data.ServiceName) }