Timeleap auto setup app

Edit: I have the program running and everything works on my Macs. If you want to try it out and see if it works for you, let me know.

import SwiftUI
import Foundation
import Dispatch


struct ContentView: View {
    @State private var logText: String = "Welcome to Timeleap Setup!\nDownload the CLI, then run the setup.\n"
    @State private var statusText: String = "Status: Idle\n"
    @State private var monitoringText: String = "Monitoring Output...\n"
    
    @State private var isWorkerRunning: Bool = false
    @State private var isBrokerRunning: Bool = false
    @State private var releaseVersion: String = "v0.14.0-alpha.2"
    
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("Timeleap Devnet Setup").font(.title).padding(.top, 20)
            
            // Add a TextField for the release version
            HStack {
                Text("Release Version:")
                TextField("Enter release version", text: $releaseVersion)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .frame(width: 200)
            }
            
            HStack(spacing: 30) {
                Button(action: { downloadTimeleapCLI() }) {
                    Text("Download Timeleap CLI")
                        .font(.headline)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                Button(action: { setupDevnet() }) {
                    Text("Setup Timeleap Devnet")
                        .font(.headline)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                Button(action: { toggleBrokerNode() }) {
                    Text(isBrokerRunning ? "Stop Broker Node" : "Start Broker Node")
                        .font(.headline)
                        .padding()
                        .background(isBrokerRunning ? Color.green : Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                Button(action: { toggleWorkerNode() }) {
                    Text(isWorkerRunning ? "Stop Worker Node" : "Start Worker Node")
                        .font(.headline)
                        .padding()
                        .background(isWorkerRunning ? Color.green : Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                
                Button(action: { clearLogs() }) {
                    Text("Clear Logs")
                        .font(.headline)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }
            .buttonStyle(PlainButtonStyle())
            VStack(alignment: .leading, spacing: 10) {
                Text("Logs").font(.headline)
                TextEditor(text: $logText)
                    .frame(height: 150)
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(10)
            }
            VStack(alignment: .leading, spacing: 10) {
                Text("Node Status").font(.headline)
                TextEditor(text: $statusText)
                    .frame(height: 100)
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(10)
            }
            VStack(alignment: .leading, spacing: 10) {
                Text("Monitoring").font(.headline)
                TextEditor(text: $monitoringText)
                    .frame(height: 120)
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(10)
            }
        }
        .padding()
        .onAppear {
            monitorLogs()
        }
    }
    
    func downloadTimeleapCLI() {
        let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
        let timeleapDir = "\(homeDirectory)/timeleap"
        let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"

        let osName: String
        #if os(macOS)
        osName = "darwin"
        #elseif os(Linux)
        osName = "linux"
        #elseif os(Windows)
        osName = "windows"
        #else
        osName = "unknown"
        #endif

        let architecture = ProcessInfo.processInfo.machineHardwareName
        let archName: String
        if architecture.contains("arm64") {
            archName = "arm64"
        } else if architecture.contains("x86_64") || architecture.contains("amd64") {
            archName = "amd64"
        } else {
            archName = "unknown"
        }

        // Use the releaseVersion state variable in the URL
        let timeleapURL = "https://github.com/TimeleapLabs/timeleap/releases/download/\(releaseVersion)/timeleap.\(osName).\(archName)"
        logText += "Download URL: \(timeleapURL)\n"

        runShellCommand("mkdir -p \(timeleapDir)")
        logText += "Ensuring directory exists: \(timeleapDir)...\n"

        let curlCommand = "curl -L -o \(timeleapPath) \(timeleapURL)"
        logText += "Running command: \(curlCommand)\n"
        let curlOutput = runShellCommandAndReturnOutput(curlCommand)
        logText += "curl output: \(curlOutput)\n"

        if FileManager.default.fileExists(atPath: timeleapPath) {
            runShellCommand("chmod +x \(timeleapPath)")
            logText += "✅ Timeleap CLI downloaded and made executable!\n"
        } else {
            logText += "❌ ERROR: Failed to download Timeleap CLI.\n"
        }
    }
    
    // Rest of the functions remain unchanged...



    
    // Toggle Broker Node
    func toggleBrokerNode()
    {let debugOutput = runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'")
        self.logText += "Broker PIDs at toggle: \(debugOutput)\n"

        if isBrokerRunning {
            isBrokerRunning = false // Update state immediately
            stopBrokerNode()
        } else {
            isBrokerRunning = true // Update state immediately
            startBrokerNode()
        }
    }

    // Toggle Worker Node
    func toggleWorkerNode() {
        if isWorkerRunning {
            isWorkerRunning = false // Update state immediately
            stopWorkerNode()
        } else {
            isWorkerRunning = true // Update state immediately
            startWorkerNode()
        }
    }
        
    func startBrokerNode() {
        let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
        let timeleapDir = "\(homeDirectory)/timeleap"
        let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
        let logFilePath = "\(timeleapDir)/timeleap.log"
        
        DispatchQueue.global().async {
            DispatchQueue.main.async {
                self.statusText += "Starting Broker Node...\n"
            }
            
            // Run broker in background
            self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) broker >> \(logFilePath) 2>&1 &")
            
            // Wait for the broker process to start
            var retryCount = 5
            while retryCount > 0 {
                let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'").trimmingCharacters(in: .whitespacesAndNewlines)
                if !output.isEmpty {
                    // Broker process is running
                    DispatchQueue.main.async {
                        self.isBrokerRunning = true
                        self.statusText += "Broker Node started.\n"
                    }
                    return
                }
                Thread.sleep(forTimeInterval: 1) // Wait before checking again
                retryCount -= 1
            }
            
            // If the broker process didn't start
            DispatchQueue.main.async {
                self.statusText += "❌ ERROR: Broker Node failed to start.\n"
            }
        }
    }


    func stopBrokerNode() {
        Foundation.DispatchQueue.global().async {
            Foundation.DispatchQueue.main.async {
                self.statusText += "Stopping Broker Node...\n"
            }

            // Kill the process
            self.runShellCommand("pkill -f 'timeleap broker'")

            // Ensure the process is actually stopped
            var retryCount = 5
            while retryCount > 0 {
                let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'").trimmingCharacters(in: .whitespacesAndNewlines)
                if output.isEmpty {
                    break // Process is fully stopped
                }
                Thread.sleep(forTimeInterval: 1) // Wait before checking again
                retryCount -= 1
            }

            // Confirm it's stopped before updating the UI
            Foundation.DispatchQueue.main.async {
                self.isBrokerRunning = false
                self.statusText += "Broker Node stopped.\n"
            }
        }
    }



        
        // Start Worker Node
    func startWorkerNode() {
        let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
        let timeleapDir = "\(homeDirectory)/timeleap"
        let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
        let logFilePath = "\(timeleapDir)/timeleap.log"
        
        DispatchQueue.global().async {
            DispatchQueue.main.async {
                self.statusText += "Starting Worker Node...\n"
            }
            
            // Run worker in background
            self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) worker >> \(logFilePath) 2>&1 &")
            
            // Wait for the worker process to start
            var retryCount = 5
            while retryCount > 0 {
                let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap worker'").trimmingCharacters(in: .whitespacesAndNewlines)
                if !output.isEmpty {
                    // Worker process is running
                    DispatchQueue.main.async {
                        self.isWorkerRunning = true
                        self.statusText += "Worker Node started.\n"
                    }
                    return
                }
                Thread.sleep(forTimeInterval: 1) // Wait before checking again
                retryCount -= 1
            }
            
            // If the worker process didn't start
            DispatchQueue.main.async {
                self.statusText += "❌ ERROR: Worker Node failed to start.\n"
            }
        }
    }
        
    func stopWorkerNode() {
        DispatchQueue.global().async {
            DispatchQueue.main.async {
                self.statusText += "Stopping Worker Node...\n"
            }

            // Kill the process
            self.runShellCommand("pkill -f 'timeleap worker'")

            // Ensure the process is actually stopped
            var retryCount = 5
            while retryCount > 0 {
                let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap worker'").trimmingCharacters(in: .whitespacesAndNewlines)
                if output.isEmpty {
                    break // Process is fully stopped
                }
                Thread.sleep(forTimeInterval: 1) // Wait before checking again
                retryCount -= 1
            }

            // Confirm it's stopped before updating the UI
            DispatchQueue.main.async {
                self.isWorkerRunning = false
                self.statusText += "Worker Node stopped.\n"
            }
        }
    }

    
    // Clear Logs
       func clearLogs() {
           logText = ""
           statusText = "Status: Idle\n"
           monitoringText = "Monitoring Output...\n"
           
           let logFilePath = FileManager.default.homeDirectoryForCurrentUser.path + "/timeleap/timeleap.log"
           
           do {
               if FileManager.default.fileExists(atPath: logFilePath) {
                   try FileManager.default.removeItem(atPath: logFilePath)
               }
               FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil)
               logText += "✅ Log file cleared.\n"
           } catch {
               logText += "❌ ERROR: Failed to clear log file - \(error.localizedDescription)\n"
           }
       }
       
       // Monitor Logs
       func monitorLogs() {
           let logFilePath = FileManager.default.homeDirectoryForCurrentUser.path + "/timeleap/timeleap.log"
           
           DispatchQueue.global(qos: .background).async {
               while true {
                   if FileManager.default.fileExists(atPath: logFilePath) {
                       if let logs = try? String(contentsOfFile: logFilePath, encoding: .utf8) {
                           DispatchQueue.main.async {
                               self.monitoringText = logs
                           }
                       }
                   } else {
                       DispatchQueue.main.async {
                           self.monitoringText = "⚠️ No logs found. Is Timeleap running?"
                       }
                   }
                   Thread.sleep(forTimeInterval: 2)
               }
           }
       }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    func setupDevnet() {
        logText += "Starting setup...\n"
        statusText = "Status: Setting up...\n"
        
        DispatchQueue.global().async {
            self.checkAndInstallDependencies()
            
            let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
            let timeleapDir = "\(homeDirectory)/timeleap"
            let confYamlPath = "\(timeleapDir)/conf.yaml"
            let secretsYamlPath = "\(timeleapDir)/secrets.yaml"
            let logFilePath = "\(timeleapDir)/timeleap.log"
            let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
            
            if !FileManager.default.fileExists(atPath: timeleapPath) {
                DispatchQueue.main.async {
                    self.logText += "⚠️ Timeleap CLI not found. Please download it first.\n"
                }
                return
            }

            self.runShellCommand("mkdir -p \(timeleapDir)")
            self.runShellCommand("touch \(logFilePath)")
            self.runShellCommand("rm -rf \(timeleapDir) && mkdir -p \(timeleapDir)")
            self.runShellCommand("chmod -R 755 \(timeleapDir)") // Set permissions

            if !FileManager.default.fileExists(atPath: timeleapDir) {
                DispatchQueue.main.async {
                    self.logText += "Error: Failed to download Timeleap CLI.\n"
                }
                return
            }
            
            self.runShellCommand("file \(timeleapDir)")
            self.runShellCommand("ls -l \(timeleapDir)")
            self.runShellCommand("file \(timeleapDir)")
            self.runShellCommand("chmod +x \(timeleapPath)")

            let initialConfYaml = """
            system:
              log: info
              name: Timeleap

            network:
              bind: 0.0.0.0:9123
              broker:
                uri: ws://localhost:9123
                publicKey: "publicKey"

            rpc:
              cpus: 1
              gpus: 0
              ram: 1024

            pos:
              rpc:
                - https://arbitrum.llamarpc.com
            """

            do {
                try initialConfYaml.write(toFile: confYamlPath, atomically: true, encoding: .utf8)
                DispatchQueue.main.async {
                    self.logText += "✅ Initial conf.yaml written successfully!\n"
                }
            } catch {
                DispatchQueue.main.async {
                    self.logText += "❌ ERROR: Could not write initial conf.yaml\n"
                }
                return
            }

            DispatchQueue.main.async {
                self.logText += "🔑 Generating secrets.yaml...\n"
            }

            // Check for port conflicts
            let portCheckCommand = "lsof -i :9123"
            let portCheckOutput = self.runShellCommandAndReturnOutput(portCheckCommand)
            DispatchQueue.main.async {
                self.logText += "Port check output: \(portCheckOutput)\n"
            }

            if !portCheckOutput.isEmpty {
                self.runShellCommand("kill -9 $(lsof -t -i :9123)")
                DispatchQueue.main.async {
                    self.logText += "Terminated processes using port 9123.\n"
                }
            }

            // Terminate any existing broker process
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) { // Wait 5 seconds
                self.runShellCommand("pkill -f 'timeleap broker'")
            }
            

            // Run the broker process to generate secrets
            self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) broker --allow-generate-secrets")

            // Terminate the broker process after generating secrets
            self.runShellCommand("pkill -f 'timeleap broker'")

            // Check if secrets.yaml was generated
            if !FileManager.default.fileExists(atPath: secretsYamlPath) {
                DispatchQueue.main.async {
                    self.logText += "❌ ERROR: secrets.yaml was not generated. Check if the Timeleap binary is working.\n"
                }
              
            }

            // Read and update conf.yaml with the public key from secrets.yaml
            if let secretsYamlContents = try? String(contentsOfFile: secretsYamlPath, encoding: .utf8) {
                let lines = secretsYamlContents.components(separatedBy: .newlines)
                for line in lines {
                    if line.contains("publicKey:") {
                        let publicKey = line.components(separatedBy: " ")[1].trimmingCharacters(in: .whitespacesAndNewlines)
                        
                        do {
                            var confYamlContents = try String(contentsOfFile: confYamlPath, encoding: .utf8)
                            confYamlContents = confYamlContents.replacingOccurrences(of: "publicKey: \"publicKey\"", with: "publicKey: \"\(publicKey)\"")
                            try confYamlContents.write(toFile: confYamlPath, atomically: true, encoding: .utf8)
                            DispatchQueue.main.async {
                                self.logText += "✅ Updated conf.yaml with public key: \(publicKey)\n"
                            }
                        } catch {
                            DispatchQueue.main.async {
                                self.logText += "❌ ERROR: Could not update conf.yaml with public key\n"
                            }
                        }
                        break
                    }
                }
            } else {
                DispatchQueue.main.async {
                    self.logText += "❌ ERROR: Could not read secrets.yaml\n"
                }
            }

            DispatchQueue.main.async {
                self.logText += "Setup complete!\n"
                self.statusText = "Status: Ready!\n"
            }
        }
    }
    
    func runShellCommand(_ command: String) {
        let task = Process()
        let pipe = Pipe()
        let errorPipe = Pipe()

        task.standardOutput = pipe
        task.standardError = errorPipe
        task.arguments = ["-c", command]
        task.executableURL = URL(fileURLWithPath: "/bin/bash")

        do {
            try task.run()
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
            let output = String(data: data, encoding: .utf8) ?? ""
            let errorOutput = String(data: errorData, encoding: .utf8) ?? ""

            DispatchQueue.main.async {
                self.logText += "Executing: \(command)\nOutput: \(output)\nError: \(errorOutput)\n"
            }
        } catch {
            DispatchQueue.main.async {
                self.logText += "Error: \(error.localizedDescription)\n"
            }
        }
    }

    func runShellCommandAndReturnOutput(_ command: String) -> String {
        let task = Process()
        let pipe = Pipe()
        
        task.standardOutput = pipe
        task.arguments = ["-c", command]
        task.executableURL = URL(fileURLWithPath: "/bin/bash")

        var output = ""
        do {
            try task.run()
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            output = String(data: data, encoding: .utf8) ?? ""
        } catch {
            DispatchQueue.main.async {
                self.logText += "Error executing: \(command)\n"
            }
        }
        return output.trimmingCharacters(in: .whitespacesAndNewlines)
    }
   
    func checkPermissions(at path: String) -> Bool {
        return FileManager.default.isWritableFile(atPath: path)
    }
    
    func checkAndInstallDependencies() {
        if !commandExists("/opt/homebrew/bin/brew") {
            logText += "Homebrew not found. Installing Homebrew...\n"
            runShellCommand("""
            /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
            """)
        } else {
            logText += "Homebrew is already installed.\n"
        }
        
        if !commandExists("/opt/homebrew/bin/node") {
            logText += "Node.js not found. Installing Node.js...\n"
            runShellCommand("/opt/homebrew/bin/brew install node")
            runShellCommand("export PATH=/opt/homebrew/bin:$PATH")
        } else {
            logText += "Node.js is already installed.\n"
        }
        
        runShellCommand("/opt/homebrew/bin/node -v")
        
        if !commandExists("/opt/homebrew/bin/yarn") {
            logText += "Yarn not found. Installing Yarn...\n"
            runShellCommand("/opt/homebrew/bin/brew install yarn")
        } else {
            logText += "Yarn is already installed.\n"
        }
        
        if !commandExists("curl") {
            logText += "curl not found. Please install curl manually.\n"
        } else {
            logText += "curl is already installed.\n"
        }
    }
    
    func commandExists(_ command: String) -> Bool {
        let task = Process()
        let pipe = Pipe()
        
        task.standardOutput = pipe
        task.standardError = pipe
        task.arguments = ["-c", "which \(command)"]
        task.executableURL = URL(fileURLWithPath: "/bin/bash")
        
        do {
            try task.run()
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let output = String(data: data, encoding: .utf8) ?? ""
            return !output.isEmpty
        } catch {
            return false
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

extension Data {
    init?(hexString: String) {
        let length = hexString.count / 2
        var data = Data(capacity: length)
        var index = hexString.startIndex

        for _ in 0..<length {
            let nextIndex = hexString.index(index, offsetBy: 2)
            if let byte = UInt8(hexString[index..<nextIndex], radix: 16) {
                data.append(byte)
            } else {
                return nil
            }
            index = nextIndex
        }
        self = data
    }
}

extension ProcessInfo {
    var machineHardwareName: String {
        var systemInfo = utsname()
        uname(&systemInfo)
        let machineMirror = Mirror(reflecting: systemInfo.machine)
        let identifier = machineMirror.children.reduce("") { identifier, element in
            guard let value = element.value as? Int8, value != 0 else { return identifier }
            return identifier + String(UnicodeScalar(UInt8(value)))
        }
        return identifier
    }
}

1 Like

Added a configuration window.

Well, I guess the code is too long for the forum now. I have moved it to Github which is great because it gives me more experience using something new.

2 Likes