Delayed Return in Swift

So

I have an app that users can create utilities with some shell script

I have a feature that the user can experiment with their scripts (a shell REPL)

But if I type zsh in the REPL the whole app went stuck and the zsh shell outputs in Xcode:

(This is my zsh theme)

I've added detection for these:

But what I really want is to let the process run in the background while the REPL output "Time out waiting for pipeline output after xxx secs" after xxx secs

I've thought about letting them run in two separate asynchronous tasks so that if one task was completed it could first return the function but I just can't manage it:

func run(launchPath: String? = nil, command: String) -> String {
    let task = Process()
    let pipe = Pipe()

    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.launchPath = launchPath ?? "/bin/zsh/"
    task.launch()
    Task {
        let data = try? pipe.fileHandleForReading.readToEnd()!
        return String(data: data!, encoding: .utf8)!
    }
    DispatchQueue.main.asyncAfter(deadline: .now + 30) {
        return "Time out" // ERROR
    }
}

So

How can I create two separate asynchronous tasks, one receiving the pipe output, and one, after a few seconds, returns the function?

I’m not sure I fully understand what you’re asking for here. My best guess is that you’re trying to create something like the macOS Terminal app, that is, you want to:

  • Start a child process.

  • Monitor for it termination.

  • Accept user input and forward it to the process’s stdin.

  • Monitor the process’s stdout and stderr and display that within your app.

Is that right?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Some additional info @eskimo

import SwiftUI

let interactive = [
    "zsh",
    "bash",
    "vi",
    "vim",
    "top"
]
struct REPLView: View {
    @AppStorage("launch") var launchPath: String = "/bin/zsh"
    @State var context: Text = Text("SHELL STARTED AT ")
        .fontWeight(.black) + Text("\(Date().ISO8601Format())\n")
        .foregroundColor(.accentColor)
        .fontWeight(.bold)
    @State var command: String = ""
    var body: some View {
        VStack {
            ScrollView([.horizontal, .vertical]) {
                HStack {
                    context
                    Spacer()
                }
                .frame(width: 500)
                .padding()
            }
            .frame(width: 500, height: 300)
            .border(.gray)
            HStack {
                TextField("@\(launchPath)", text: $command)
                    .onSubmit {
                        runCommand()
                    }
                    .textFieldStyle(.plain)
                Button {
                    runCommand()
                } label: {
                    Label("Send", systemImage: "arrow.right")
                        .foregroundColor(.accentColor)
                }
                .buttonStyle(.plain)
            }
            .padding()
        }
        .onAppear {
            newPrompt()
        }
    }
    func newPrompt() {
        var new: Text {
            let new = Text("Utilities REPL@*\(Host.current().name!)* \(launchPath)")
                .fontWeight(.bold)
            let prompt = Text(" > ")
            return context + new + prompt
        }
        context = new
    }
    func runCommand() {
        if interactive.contains(command
            .replacingOccurrences(of: " ", with: "")
            .replacingOccurrences(of: "\t", with: "")
            .replacingOccurrences(of: "\n", with: "")) {
            var new: Text {
                let commnd = Text(" \(command)\n")
                let cont: Text = Text("*The REPL have not yet supported `\(command)` inside the environment.*\n")
                    .foregroundColor(.red)
                return context + commnd + cont
            }
            context = new
            newPrompt()
        } else {
            var new: Text {
                let commnd = Text(" \(command)\n")
                let cont: Text = Text(run(command: command))
                return context + commnd + cont
            }
            context = new
            newPrompt()
        }
    }
}
import Foundation

struct Utility: Codable, Hashable, Identifiable {
    let id: UUID
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(Utility.self, from: data)
        else { return nil }
        self.name = result.name
        self.asyncFetch = result.asyncFetch
        self.symbol = result.symbol
        self.command = result.command
        self.id = result.id
    }
    public init(name: String? = nil, command: String? = nil, symbol: String? = nil, asyncFetch: Bool? = nil) {
        self.name = name ?? "New Utility"
        self.command = command ?? #"echo "Command "# + (name ?? "New Utility") + #" Executed""#
        self.symbol = symbol ?? symbols.randomElement()!
        self.asyncFetch = asyncFetch ?? true
        self.id = .init()
    }
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.command = try container.decode(String.self, forKey: .command)
        self.symbol = try container.decode(String.self, forKey: .symbol)
        self.asyncFetch = try container.decode(Bool.self, forKey: .asyncFetch)
        self.id = try container.decode(UUID.self, forKey: .id)
    }
    public init?(_ script: URL) {
        guard let content = try? String(contentsOf: script) else {
            return nil
        }
        var scriptText = ""
        for i in content.split(separator: "\n") {
            var j: String = String(i)
            while i.first == " " || i.first == "\t" {
                j.removeFirst()
            }
            while i.last == " " || i.last == "\t" {
                j.removeLast()
            }
            if j.first == "#" {
                continue
            }
            scriptText += (j + "; ")
        }
        self.name = script.deletingPathExtension().lastPathComponent
        self.command = scriptText
        self.symbol = symbols.randomElement()!
        self.asyncFetch = true
        self.id = .init()
    }
    public var rawValue: String {
        var encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        guard let data = try? encoder.encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return ""
        }
        return result
    }
    var name: String
    var command: String
    var symbol: String
    var asyncFetch: Bool
    @discardableResult
    func run(launchPath: String? = nil) -> String {
        let output = Utilities.run(command: command)
        return output
    }
    func run(logFile: inout String, launchPath: String? = nil, stripeDeadCharacters: Bool? = nil) {
        var output = Utilities.run(command: command)
        if stripeDeadCharacters ?? false {
            while output.last == "\n" {
                output.removeLast()
            }
        }
        logFile += output
    }
}

extension Array: RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data)
        else { return nil }
        self = result
    }
    
    public var rawValue: String {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        guard let data = try? encoder.encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

Some additional info

I’d like to focus on the big picture, rather than your code. Apropos that, you wrote:

The REPL part, yes but it's not the main part of the app. If the user creates a Utility and it has a command like top it'll be stuck

I’m still not getting this. It seems you have two separate concepts:

  • “The REPL”

  • “a Utility”

You’re using these terms as if I should understand them, but I know nothing about your app except what you’ve posted here )-: Please elaborate on the user experience you’re looking for.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Delayed Return in Swift
 
 
Q