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 *** secs" after *** 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