builtin-programs/unix-commands.folk

# Spawns a Unix command to stream output lines back to the Wisher.
#
# Wish $p runs Unix command "echo" with arguments [list "Hello" "World"]
# Wish $this runs Unix command "journalctl" with arguments [list "-f" "-u" "folk"]
When /someone/ wishes /p/ runs Unix command /command/ with arguments /args/ {
	set outputKeyName [list unix-output $p]
	set errorKeyName [list unix-error $p]
	set maxLines 500
	set maxLinesEndIndex [expr {$maxLines - 1}]
	set accumulatedLines [list]

	# Bound how many lines we drain per tick to avoid starvation under heavy output
	set maxLinesPerTick 10

	# Build argv as a flat list and form pipeline tokens
	set flatArgs [concat {*}$args]
	set argv [list $command {*}$flatArgs]
	set pipeline [linsert $argv 0 |]

	try {
		# Start the process with STDERR merged into STDOUT
		lappend pipeline 2>@1
		set fd [open $pipeline r]

		fconfigure $fd -blocking 0 -buffering none

		set pids [pid $fd]
		set pid [lindex $pids end]
	} on error e {
		puts "Failed to open '$command': $e"

		Hold! -key $errorKeyName \
			Claim $p has Unix error output $e

		return
	}

	On unmatch [list apply {{fd pid} {
		catch {close $fd}
		catch {kill SIGTERM $pid}
		after 500
		catch {kill SIGKILL $pid}
	} } $fd $pid]

	while true {
		set newLines [list]
		set drained 0

		while {$drained < $maxLinesPerTick} {
			set num [gets $fd line]
			if {$num < 0} { break }

			lappend newLines $line
			incr drained
		}

		if {[llength $newLines] > 0} {
			set accumulatedLines [concat $accumulatedLines $newLines]
			set length [llength $accumulatedLines]

			if {$length > $maxLines} {
				set accumulatedLines [lrange $accumulatedLines end-$maxLinesEndIndex end]
			}

			Hold! -key $outputKeyName \
				Claim $p has Unix output lines $accumulatedLines
		}

		if {[eof $fd]} {
			# Emit any last partial unterminated lines
			set tail [read $fd]

			if {$tail ne ""} {
				set accumulatedLines [concat $accumulatedLines [list $tail]]

				Hold! -key $outputKeyName \
					Claim $p has Unix output lines $accumulatedLines
			}

			if {[catch {close $fd} err]} {
				puts "Close error for '$command': $err"

				Hold! -key $errorKeyName \
					Claim $p has Unix error output $err
			}

			break
		}

		# Sleep for a bit to avoid starving under heavy output
		after 100
	}
}

# Convenience wrapper for commands without arguments
When /wisher/ wishes /p/ runs Unix command /command/ {
	Say $wisher wishes $p runs Unix command $command with arguments [list]
}

# When /someone/ wishes /p/ tests Unix commands {
# 	# Wish $p runs Unix command "echo" with arguments [list "Hello" "World"]
# 	# Wish $p runs Unix command "curl" with arguments [list "-fsS" "http://wttr.in/Baltimore?format='%l:+%C'"]
# 	# Wish $p runs Unix command "ls" with arguments [list "-sSh" "/home/folk/folk2"]
# 	# Wish $p runs Unix command "ping" with arguments [list "google.com"]
# 	# Wish $p runs Unix command "sh" with arguments [list "-c" "while :; do date +%s.%3N; sleep 0.5; done"]

# 	# Test error handling:
# 	# Wish $p runs Unix command "ls" with arguments [list "/nonexistent/path"]
# 	# Wish $p runs Unix command "exec" with arguments [list "/dev/null"]

# 	When $p has Unix error output /errorSummary/ {
# 		puts "errorSummary: $errorSummary"

# 		Wish $p is labelled [join $errorSummary "\n"]
# 		Wish $p is outlined red
# 	}

# 	When $p has Unix output lines /outputLines/ {
# 		puts "outputLines: $outputLines"

# 		Wish $p is labelled [join $outputLines "\n"]
# 		Wish $p is outlined green
# 	}
# }