Jump to content

Workflows with Run Script phases wait for daemonized processes


lilyball

Recommended Posts

When a Workflow uses a Run Script phase, Alfred seems to wait for all spawned processes to exit before considering the workflow to be finished (which at the very least prevents running the same workflow a second time until the first invocation is considered done).

 

The problem is that it's waiting for stuff it shouldn't. Specifically, daemonized processes (which is to say, processes that forked and abandoned the process group) are causing Alfred to continue waiting, even though the entire point of daemonizing is to detach from the controlling terminal (and in this case Alfred acts like the controlling terminal).

 

The specific case I have here is launching MacVim. When launching Vim in GUI mode, it forks and abandons the process group, and the parent process dies. This is done explicitly so the terminal that launched it will return immediately to the command line instead of waiting on the process (it also allows the GUI Vim process to outlive the terminal). But this doesn't work in Alfred.

 

Here's the spawned process log:

2014 Jun 10 17:12:32 71064 <430> 64b: /usr/local/Cellar/macvim/7.4-72/MacVim.app/Contents/MacOS/Vim -g -c setf test -c startinsert
2014 Jun 10 17:12:32 71101 <1> 64b: /usr/local/Cellar/macvim/7.4-72/MacVim.app/Contents/MacOS/Vim -f -g -c setf test -c startinsert

That first process there, PID 71064, was spawned by Alfred (PID 430). It then forks and runs the second process, PID 71101, which as you can see has a PPID of 1, because it detached from the process group. 71064 then immediately exits (it's not visible in this log, but I verified it with `ps`).

 

However, Alfred will continue the workflow to be running for as long as PID 71101 lives. If I turn on workflow debugging and add an `echo`, it doesn't print the output until I quit Vim. Similarly, if I try and invoke the workflow a second time, it doesn't actually run the script phase for the second invocation until I've quit the Vim process from the first one.

Link to comment
Share on other sites

It's unlikely that I'll be changing Alfred's behaviour as intrinsically, Alfred waits for a process to finish to gather the output from it. Sub daemon process behaviour is controlled by NSTask.

 

I'm going to move this to noted, but for now, I'm not going to treat this as a bug (especially since there is a workaround).

Link to comment
Share on other sites

Andrew, I just built a quick test of NSTask behavior under these exact same circumstances, and `waitUntilExit` does indeed return as soon as the "outer" Vim process exits. I'm guessing you're actually waiting until the process stdout pipe has closed? I suppose that suggests the workaround: set stdout to /dev/null. But it also suggests that, once the task has terminated, Alfred should be able to just read any remaining information in the stdout/stderr pipes, and then immediately close them. Adopting this behavior will only affect workflows that spawn sub-processes and exit the parent process, and since doing that normally kills the subprocess as well, it means it really should only affect situations like MacVim where the process daemonizes itself.

 

Sample code:

import Foundation

func runTask() {
	var task = NSTask()
	task.launchPath = "/Applications/MacVim.app/Contents/MacOS/Vim"
	task.arguments = ["-g"]

	println("Launching task...")
	task.launch()
	println("PID: \(task.processIdentifier)")
	println("Waiting for termination...")
	task.waitUntilExit()
	println("Exit status: \(task.terminationStatus)")

	CFRunLoopStop(CFRunLoopGetCurrent())
}

CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes) {
	runTask()
}

CFRunLoopRun()
Edited by kballard
Link to comment
Share on other sites

 

Andrew, I just built a quick test of NSTask behavior under these exact same circumstances, and `waitUntilExit` does indeed return as soon as the "outer" Vim process exits. I'm guessing you're actually waiting until the process stdout pipe has closed? I suppose that suggests the workaround: set stdout to /dev/null. But it also suggests that, once the task has terminated, Alfred should be able to just read any remaining information in the stdout/stderr pipes, and then immediately close them. Adopting this behavior will only affect workflows that spawn sub-processes and exit the parent process, and since doing that normally kills the subprocess as well, it means it really should only affect situations like MacVim where the process daemonizes itself.

 

Sample code:

import Foundation

func runTask() {
	var task = NSTask()
	task.launchPath = "/Applications/MacVim.app/Contents/MacOS/Vim"
	task.arguments = ["-g"]

	println("Launching task...")
	task.launch()
	println("PID: \(task.processIdentifier)")
	println("Waiting for termination...")
	task.waitUntilExit()
	println("Exit status: \(task.terminationStatus)")

	CFRunLoopStop(CFRunLoopGetCurrent())
}

CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes) {
	runTask()
}

CFRunLoopRun()

 

You are correct, Alfred doesn't use waitUntilExit, he waits until stdout pipe is closed.

 

When I originally used waitUntilExit (Alfred v1), I was seeing hanging / performance issues which ultimately affected the perceptual speed of Alfred. I switched to stdout closing as a marker of being done and haven't seen a performance issue since. This may no longer be relevant in newer versions of OS X and whatnot, but as there isn't much of a call for this, I'm reluctant to make any change in this area.

 

Cheers,

Andrew

Link to comment
Share on other sites

You are correct, Alfred doesn't use waitUntilExit, he waits until stdout pipe is closed.

 

When I originally used waitUntilExit (Alfred v1), I was seeing hanging / performance issues which ultimately affected the perceptual speed of Alfred. I switched to stdout closing as a marker of being done and haven't seen a performance issue since. This may no longer be relevant in newer versions of OS X and whatnot, but as there isn't much of a call for this, I'm reluctant to make any change in this area.

 

Cheers,

Andrew

 

You could try waiting until the pipe is closed, and also setting a terminationHandler on the NSTask. If the termination handler fires while the pipe is still open, you could then just drain any available data from the pipe and close it. That should leave the current performance intact (because if, as you suggest, the pipe closes before the task finishes terminating, then it should close before terminationHandler fires), but allow for handling cases like what I've described.

Link to comment
Share on other sites

You could try waiting until the pipe is closed, and also setting a terminationHandler on the NSTask. If the termination handler fires while the pipe is still open, you could then just drain any available data from the pipe and close it. That should leave the current performance intact (because if, as you suggest, the pipe closes before the task finishes terminating, then it should close before terminationHandler fires), but allow for handling cases like what I've described.

 

Interesting, thanks! I'll add a ticket to look into this / keep it in mind, but as we still support 10.6, the terminationHandler isn't currently a holistic solution... I could just make this a 10.7+ only fix but it may lead to unexpected or difficult to debug workflow behaviour for some workflow creators.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...