Foundation's Process: ps process only works with some flags [Solved]

Hello,
I was wondering if anyone could help explain something to me.

I am attempting to use Foundation's Process to run 'ps' to list active processes.

I have the following code as listed at the bottom of this post.

If the line marked //1 is unaltered then the process runs 'ps -axc -o comm' and works. The process runs and completes and stdOut contains the output.

Testing Foundation's Process with 'ps'
ps5(alternateFlags:)
running file:///bin/ps ["-axc", "-o comm"]
ps5(alternateFlags:) ends.
terminationHandler
ps process was terminated (NSTaskTerminationReason(rawValue: 1)
StdOut:
COMM
launchd
logd
smd
UserEventAgent
<.... Lots of processes snipped here .... >

However, if in line //1 the alternateFlags argument is set to true then the process runs 'ps -ax' and doesn't work. The process runs and never completes.

Testing Foundation's Process with 'ps'
ps5(alternateFlags:)
running file:///bin/ps ["-ax"]
ps5(alternateFlags:) ends.
Program ended with exit code: 9

Why is this? Is it a macOS sandbox thing? Both versions work on Linux. (n.b. the code needs to be altered slightly on Linux to compile correctly due to missing API in Foundation.)

import Foundation
print("Testing Foundation's Process with 'ps'")

let psBinaryPath = "/bin/ps"


let app = App()
app.run()

struct App {
	
	let runLoop = RunLoop.current
	let distantFuture = Date.distantFuture
	///	Set this to false when we want to exit the app...
	let shouldKeepRunning = true
	
	func run() {
// 1
		ps5(alternateFlags: false)
		
		//	Run forever
		while shouldKeepRunning == true &&
				runLoop.run(mode:.default, before: distantFuture) {}
		
	}
}

func ps5(alternateFlags: Bool = false) {
	print(#function)

	let stdOutPipe = Pipe()
	let stdErrPipe = Pipe()
	
	let psProcess = Process()
	//	Using executableURL, which is more modern.
	if #available(macOS 13.0, *) {
		psProcess.executableURL = URL(filePath: psBinaryPath)
	} else {
		psProcess.executableURL = URL(fileURLWithPath: psBinaryPath)
	}
	
//2			//	This Process ends
	psProcess.arguments = ["-axc", "-o comm"]

	if alternateFlags {
//3			//	This Process doesn't end
			psProcess.arguments = ["-ax"]
	}

	psProcess.standardOutput = stdOutPipe
	psProcess.standardError = stdErrPipe
	
	
	//	What happens when process ends.
	psProcess.terminationHandler = { process in
		print("terminationHandler")
		print("ps process was terminated (\(process.terminationReason)")
		
		if process.terminationReason != .exit {
			print("ps process ended badly")
			let outputData = stdErrPipe.fileHandleForReading.readDataToEndOfFile()
			let outputString = String(data: outputData, encoding: .utf8) ?? "No StdErr available"
			print(outputString)
			return
		}
	
		print("StdOut:")
		let outputData = stdOutPipe.fileHandleForReading.readDataToEndOfFile()
		let outputString = String(data: outputData, encoding: .utf8) ?? "No StdOut available"
		print(outputString)
	}

	//	Run the process
	do {
		print("running \(psProcess.executableURL!) \(psProcess.arguments!)")
		try psProcess.run()
	} catch {
		print("err: \(error)")
	}
	
	print("\(#function) ends.")
}

Did you also change shouldKeepRunning to false when you tested this?

FWIW, I had to fix several issues in your example to get it to compile:

  • PSProcess is not defined.
  • psBinaryPath is not defined.

Please make sure your examples compile when sharing them.

Apologies - I had trimmed the example down from an example with several options and accidentally removed a property and left another in.

I have updated the sample.

No, I left shouldKeepRunning to true so that the code would continue whilst the subprocess executed.

OK, I ran your updated sample and it’s behaving correctly. The relevant RunLoop documentation warns:

Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop.

You need to set shouldKeepRunning to false in your termination handler.

I’m not sure if this is the root cause or not, but shouldn’t "-o comm" be two arguments? It’s one option to ps, but it’s two separate strings.

Thanks. It appears to function the same way if -o comm is one argument or two. Either way, that's the set of arguments that works correctly.

1 Like

It's working correctly even if you alter the line marked //1 to pass false?

If we ignore the Runloop aspect for now, here's a version as a simple AppKit app.
You should see that the behaviour is different depending on whether you have the checkbox checked or not.

To be clear - the working options are the ones I need, but I am curious why the other set of options doesn't work, or even end.

Ah, it works correctly if I pass false, but it doesn’t print anything if I pass true.

This seems worth a bug report through Feedback Assistant.

1 Like

Thanks for testing it.

Both versions work on macOS 14, with the following steps:

  1. try psProcess.run()

  2. Read from stdout and stderr (outside of terminationHandler).

  3. psProcess.waitUntilExit() to poll the run loop.

  4. Check terminationReason and terminationStatus.

The documentation isn't clear on how to use these APIs, so I'm only guessing that terminationHandler shouldn't be used for reading data.


UPDATE: Perhaps the original issue was process deadlock.

  • The pipe had an underlying buffer of a fixed size, determined by macOS?

  • The ps -ax subprocess filled the buffer, and waited for you to read some data?

  • However, you were waiting until the subprocess exited, before reading all of the data.

1 Like

Thanks Ben, very enlightening. So, if I understand correctly, the correct model should be as follows:

  1. Run the process.
  2. Treat the pipes like network connections, i.e. keep reading data from them / drain them as the process runs.
  3. Treat the termination handler solely as a signal that the process has ended, not as an equivalent of a function ‘return’.

Hurrah, it works! Thanks very much.


func ps5(alternateFlags: Bool = false) {
	print(#function)

	let stdOutPipe = Pipe()
	let stdErrPipe = Pipe()
	
	let psProcess = Process()
	if #available(macOS 13.0, *) {
		psProcess.executableURL = URL(filePath: psBinaryPath)
	} else {
		psProcess.executableURL = URL(fileURLWithPath: psBinaryPath)
	}
	
	psProcess.arguments = ["-axc", "-o", "comm"]

	if alternateFlags {
			psProcess.arguments = ["-ax"]
	}

	psProcess.standardOutput = stdOutPipe
	psProcess.standardError = stdErrPipe
	
	//	What happens when process ends.
	psProcess.terminationHandler = { process in
		print("terminationHandler")
		print("ps process was terminated (\(process.terminationReason)")
		
		if process.terminationReason != .exit {
			print("ps process ended badly")
			let outputData = stdErrPipe.fileHandleForReading.readDataToEndOfFile()
			let outputString = String(data: outputData, encoding: .utf8) ?? "No StdErr available"
			print(outputString)
			return
		}

		print("StdOut:")
		let outputData = stdOutPipe.fileHandleForReading.readDataToEndOfFile()
		let outputString = String(data: outputData, encoding: .utf8) ?? "No StdOut available"
		print(outputString)

		app.shouldKeepRunning = false
	}

	//	Run the process
	do {
		print("running \(psProcess.executableURL!) \(psProcess.arguments!)")
		try psProcess.run()
	} catch {
		print("err: \(error)")
	}
	
	//	Drain stdOut whilst the process runs.
	NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: nil, queue: nil) { note in
		print("data available in stdOut.")
		let outputData = stdOutPipe.fileHandleForReading.readDataToEndOfFile()
		let outputString = String(data: outputData, encoding: .utf8) ?? "No StdOut available"
		print(outputString)
	}
	
	//	Drain stdOut whilst the process runs.
	stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
	psProcess.waitUntilExit()
	
	print("\(#function) ends.")
}

So, if I understand correctly, the correct model should be as follows:

Yes. I have a standard wrapper that I used for this, see Running a Child Process with Standard Input and Output, and I explicitly codify that approach using a Dispatch group.

Having said that, I generally avoid using ps for this sort of thing and instead call <libproc.h> directly (-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes