Apologies if this is not the right place to post a question like I have, but I'm not sure where else to go.
I am attempting to invoke yabai through key bindings implemented in Hammerspoon. Both Hammerspoon and yabai have accessibility permissions granted. Yabai commands work fine from the terminal. But attempting to execute yabai commands, e.g. os.execute("/usr/local/bin/yabai -m window --focus west") in Hammerspoon fails with exit code 1. The Yabai log prints EVENT_HANDLER_DAEMON_MESSAGE: window --focus west, but then it hangs briefly and fails. There's nothing in the error log either.
That command can fail if there is no window to the west of the current window:
> > os.execute("yabai -m window --focus west")
could not locate a westward managed window.
nil exit 1
@alin23 Good point, but that command fails no matter what. You did inspire me to try some other commands, though. From what I've tried:
yabai -m query --windows worksyabai -m window --focus west fails (window to the west exists)yabai -m window --space 1 fails (space 1 exists)yabai -m window --display 1 fails (display 1 exists)you can run lua and run os.execute there to get the output of the command. Maybe that will help you
@alin23 All commands work from a Lua REPL running in the terminal. But further investigation indicates the problem is that yabai for whatever reason doesn't recognize the existence of the Hammerspoon console window-- it doesn't show up in yabai -m query --windows. If I trigger the aforementioned yabai commands from other windows, things work OK.
So the issue is detecting the Hammerspoon console window, then...
I'm not familiar with yabai... how does it detect windows? While hs.window hasn't been a focus of mine, I do know that there have been discussions on here about detecting the various types of windows mac apps use (decorated, floating, modal, backgrounded, (non)activating, etc.) and tweaks made to detect some but not others...
If we know what yabai is looking for to consider something a "true" window, we might be able to make some changes or at least make the necessary changes something that can be toggled.
I would assume (although I have not tested) that the Hammerspoon process fail the following check: https://github.com/koekeishiya/yabai/blob/master/src/process_manager.c#L57
The other reason a window may be ignored is if it reports an AX type of popover or unknown: https://github.com/koekeishiya/yabai/blob/master/src/event.c#L530
And the final reason is simply that we are unable to observe the following notifications for said window:
static CFStringRef ax_window_notification[] =
{
[AX_WINDOW_DESTROYED_INDEX] = kAXUIElementDestroyedNotification,
[AX_WINDOW_MINIMIZED_INDEX] = kAXWindowMiniaturizedNotification,
[AX_WINDOW_DEMINIMIZED_INDEX] = kAXWindowDeminiaturizedNotification
};
If you quit hammerspoon, run yabai with debug output enabled, then launch hammerspoon, it should be easy to spot a message saying the process / window is being ignored for whatever reason.
Easiest way to verify is to simply run yabai in a terminal session: yabai --verbose
@koekeishiya Indeed-- the message is:
process_is_observable: Hammerspoon was marked as agent! ignoring..
Ok, I'll install yabai myself tonight and see if I can figure out the changes we need to make to get this working properly.
Note that it does sort of make sense for Hammerspoon to identify as an agent, as per the description from Apple. However, other applications that we cannot and should not observe also report as an agent. See the following issue for details as to why we need to filter these types: https://github.com/koekeishiya/yabai/issues/439
In short, when a process is launched we use these attributes to decide whether or not the application is eventually going to respond to AX requests. Some applications do respond x amount of time after they are finished launching and some will simply fail. As far as I have been able to tell so far, there is no way to actually identify when a request failed because it is simply not supported, and when it fails because the application did not yet finish launching (and we just need to wait a bit).
The process filtering is in place to prevent us from hitting processes that will simply loop forever during AX notification setup.
I can confirm that @koekeishiya is correct that Hammerspoon is marked as an agent during the check at https://github.com/koekeishiya/yabai/blob/master/src/process_manager.c#L57
I tried removing lines 236-7 from /Applications/Hammerspoon.app/Contents/Info.plist:
~xml
~
And then restarted Hammerspoon (I was prompted to re-enable accessibility access for Hammerspoon, though when I opened the System Preferences it was already checked -- unchecking and rechecking it cleared this problem). After some experimentation with this change, as long as Hammerspoon is configured to show its Dock icon (hs.dockIcon(true) in the console)yabai detects Hamemrspoon as a regular app.
However, attempting to run hs.execute("/usr/local/bin/yabai -m query --windows") from the console locked up for almost three minutes... subsequent runs were almost instant, but after restarting Hammerspoon, it would be unbearably slow the first time again.
So, we've got something else going on that isn't responding as yabai expects.
I'll keep yabai installed for a while and poke around some more on occasion to see if I can narrow down the specifics, but for now I'm going to have to say that we don't have a solution to the original issue posted.
I've noticed since the yabai 3.0.1 update that Hammerspoon console is now detected by yabai- m query --windows. But I am now experiencing what @asmagill reports-- locking up of Hammerspoon for close to a minute (several seconds on subsequent runs) when running yabai -m query --windows from Hammerspoon. Running yabai -m query --windows from terminal, however, seems as fast as usual.
EDIT: On further experimentation I can see that:
Given that yabai's detection of the Hammerspoon window causes performance issues for Hammerspoon, for any Hammerspoon users out there be aware that you should start them in the order yabai > Hammerspoon until the issue is fixed.
On a more general note, it seems like Yabai's detection of a window should not depend on whether the window's app was launched before or after yabai. So perhaps this points to a broader issue?
The latest master of yabai should now correctly pick up the hammerspoon process regardless of it being started before or after yabai. See #529 for history. This should require no changes to hammerspoon. I have no clue why there is a slowdown issue though - I do think that is most likely something on hammerspoons side.
Should be fixed in the latest release. Closing this one. I assume the hammerspoon people will follow up the slowdown at some point when they have time to investigate.
Thanks for addressing the issue @koekeishiya. Unfortunately, the update has made it difficult to use Yabai from Hammerspoon because of the aforementioned problem with Hammerspoon locking up. I tried various workarounds (e.g. hs.task) but couldn't successfully figure out how to execute yabai -m query --windows within Hammerspoon without the locking problem.
So until this is fixed on the Hammerspoon end, Hammerspoon users should know to use an older version of yabai (anything up to 3.0.1). Posting this for that reason.
PS paging @asmagill ... can you suggest a workaround within Hammerspoon? I thought this might work:
function getYabaiWindows()
local output
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut)
output = stdOut
end,
{ '-m', 'query', '--windows'})
task:start():waitUntilExit()
return output
end
It did seem to prevent the blocking, but output is nil. I guess task:waitUntilExit doesn't wait for the task callback? Any way to wait until that callback executes?
If hammerspoon has socket file support you could probably re-create the message functionality (yabai -m) in lua and call that. See this function: https://github.com/koekeishiya/yabai/blob/master/src/yabai.c#L48
Thanks @koekeishiya, which socket should I be reading/writing? /tmp/yabai-sa_<user>.socket or /tmp/yabai_<user>.socket?
/tmp/yabai_<user>.socket for sending messages to yabai.
I did a full brew update and made sure I was running the latest yabai, unistalled the script additions, reinstalled them, restarted the Dock, and it seems to be working on my machine:
~~~lua
t = hs.task.new('/usr/local/bin/yabai', function(s,o,e) print(s, o, e) end, { '-m', 'query', '--windows' }):start():waitUntilExit()
0 [{
"id":18395,
"pid":49495,
"app":"Hammerspoon",
"title":"",
... lots of stuff cut for brevity ...
"shadow":1,
"zoom-parent":0,
"zoom-fullscreen":0,
"native-fullscreen":0
}]
function getYabaiWindows()
local output
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut)
output = stdOut
end,
{ '-m', 'query', '--windows'})
task:start():waitUntilExit()
return output
end
getYabaiWindows()
[{
"id":18395,
"pid":49495,
"app":"Hammerspoon",
"title":"",
... lots of stuff cut for brevity ...
"shadow":1,
"zoom-parent":0,
"zoom-fullscreen":0,
"native-fullscreen":0
}]
~~~
If it matters, I am running yabai --verbose in a terminal window and not using the service mode, but I don't think that should matter...
FWIW, Hammerspoon does support TCP and UDP sockets, but I don't know if anyone has done anything with file sockets specifically... I know I haven't.
Thanks @asmagill.
The first code block you posted only prints the output, it doesn't capture it in a variable, right? I was able to achieve this as well, my issue was getting stdout of the yabai process stored in a variable, which I was unable to do with the getYabaiWindows function I posted-- it would return nil.
Interesting that it works for you. I'm thinking the disparity is due to a race condition between output being written in the callback and getYabaiWindows returning-- on your machine output is written before return, but not on my machine. That's why I am wondering if there is a way to wait for the task callback to have executed-- or is waitUntilExit() supposed to do this? It's unclear from the docs whether it just waits till the process terminates or till the callback's been run.
The first version prints out the status code followed by the stdout and then the stderr... in my case, the status code was 0, stdout is what you see, and stderr was the empty string.
In the second version, hmm... looking at it now, I'm not totally sure why it worked for me because if I'm reading the NSTask API correctly (which is what hs.task uses in Hammerspoon), there is no guarantee that the termination handler (which invokes your callback and sets output) will have completed by the time it returns (the docs should be updated to reflect this)... maybe I'm just getting lucky and you aren't in that respect.
The "proper" way to do this would be something along the lines of...
~~~lua
function getYabaiWindows(callbackWhenDone)
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut, stdErr)
callbackWhenDone(_, stdOut, stdErr)
end,
{ '-m', 'query', '--windows'})
task:start()
return task
end
myTask = getYabaiWindows(function(_, stdOut, stdErr)
if _ ~= 0 or stdErr ~= "" then
print("Something's wrong: Result Code == " .. tostring(_) .. ", Err: " .. stdErr)
else
... do whatever you want with stdOut
end
end)
~~~
Now, because the "rest" of your code appears in the callback invoked by the task callback, you're assured of it happening after the data is actually captured (plus I added some stuff to detect errors from yabai, etc.)
Also, you need to either make task non-local, or capture it like I show above (returning it and then assigning it to myTask in the invocation; otherwise garbage collection in lua might terminate it early. Plus, by capturing it this way, if you decide it's run too long, you can always temrinate it yourself with myTask:terminate().
:waitUntilExit() is a way to make sure that nothing else occurs in Hammerspoon until the task actually completes (and by nothing else, I mean timers, hotkeys, more code to do other things, etc.) but as noted above, we are not guaranteed that it will have invoked the task's own callback yet, so it just potentially slows Hammerspoon down with little to no benefit.
If you absolutely feel that you must write everything as one code block, the next version of Hammerspoon (or a development build, if you're able to build what's currently checked into the master branch here) will support something like this:
~lua
function getYabaiWindows(callbackWhenDone)
local callbackComplete = false
local output
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut, stdErr)
callbackComplete = true -- make sure to do this, even if you later add err code (stdErr) stuff
output = stdOut
end,
{ '-m', 'query', '--windows'})
task:start()
while not callbackComplete do
-- this function doesn't exist in versions of Hammerspoon <= 0.9.78
coroutine.applicationYield()
end
return output
end
~
The new coroutine.applicationYield() allows Hammerspoon to do other things (timers, etc) while returning back to this block of code every few nanoseconds to see if the while condition has been met yet -- in this case, the setting of a boolean in the task callback indicating that it is done.
Thanks for the detailed response. I currently have calls to getYabaiWindows in several places in my code, so it would be too much hassle to change all my existing calls to the asynchronous style you suggest. But the coroutine stuff is exciting! That should work -- I guess for the time being I'll peg to yabai 3.0.1 and wait till the next version of HS is released and I can use coroutines to wait for task completion.
I'm not sure what I was thinking... the coroutine support won't help in that case because the function isn't in a coroutine...
If you truly need to wait until the result is available, your best bet really is to use hs.execute instead of hs.task.
~lua
function getYabaiWindows()
local out, status, exitType, rc = hs.execute("/usr/local/bin/yabai -m query --windows")
if status and rc == 0 then
return output
elseif exitType == "exit" then
-- the program exited normally, but with an error code in rc -- the error code is command
-- specific, so it would depend upon what return values yabai can respond with; you'll
-- need to check its docs and see what they are and what you might want to do here
-- based on the value of rc
else -- exitType == "signal"
-- the command terminated because of a signal (TERM, KILL, SEGFAULT, etc.)
-- and rc will be the number of the signal that caused the command to terminate
-- again, you can do something else here if you need to
end
end
~
I added error checking in there... if you really don't care and can reasonably assume it won't fail, then you could simplify it down to:
~lua
function getYabaiWindows()
return hs.execute("/usr/local/bin/yabai -m query --windows")
end
~
If it does fail in this simplified version, the first argument returned will be an empty string (because it just returns the results from hs.execute, all four of the return values are actually returned, but if you do something like output = getYabaiWindows() then the other three are just silently dropped.)
hs.task is designed for more complex cases -- if you specifically require stderr, if you need to programmatically provide data for stdin, if you are invoking something long running and you don't want to block or wait until it's finished to do something else while waiting for the callback function to be invoked, etc.
I'm really not sure why your original function seemed to work for me -- it really shouldn't have, the more I understand how hs.task is written. I'm not sure if we can make waitUntilExit reliably work like you want it to or not, but I'll give it some thought. It will probably be sometime next week, though, as I have other priorities I'm trying to get finished first.
However, for your use case, I really think hs.execute should be sufficient.
Thanks for all your attention to this matter @asmagill.
So, your hs.execute suggestion is actually my original implementation-- the reason I tried out hs.task is that hs.execute causes terrible lag when used to query yabai windows. As I discussed in the above thread, this is only true when yabai is aware of the Hammerspoon console, (as is now always the case after yabai 3.0.2; previously it depended on whether yabai or hammerspoon was started first).
Interestingly, executing the same yabai command from terminal does not cause this lag. So there seems to be something about running the Yabai client as a subprocess of Hammerspoon that is causing the issue. I thought that adding a level of indirection via hs.task might get around this problem, and it seems that it kinda-sorta does (yabai -m query --windows runs at normal speed and you can print the output). But it caused the new problem of being unable to access the command's output.
I just ran another experiment you might be interested in, trying an even less direct execution method of yabai from hammerspoon. hs.task is used to launch a shell, which launches yabai and redirects the output to a FIFO. Unfortunately, this has the same performance profile as plain hs.execute:
local fifoPath = '/tmp/hs-yabai-fifo'
local function yabaiCommand(cmd)
if not hs.fs.attributes(fifoPath) then
hs.execute(string.format('mkfifo %s', fifoPath))
end
local shellCmd = string.format(
'/usr/local/bin/yabai -m %s > %s', cmd, fifoPath)
hs.task.new('/bin/sh', nil, {'-c', shellCmd}):start()
local fifo = io.open(fifoPath)
local output = fifo:read('a')
fifo:close()
return output
end
Ok, yeah, I see what's happening now...
hs.execute is blocking on the Hammerspoon main application thread, waiting for the results of the command it just executed... yabai meanwhile is making AXUIElement queries against Hammerspoon and Hammerspoon's main application thread is tied up waiting for the response from the command it just executed, so it can't respond to the yabai queries...
So, yeah, hs.task is the only solution that can work from within Hammerspoon. And the only consistently reliable way to use it is with callbacks as I outlined above.
~~~lua
function getYabaiWindows(whatToDoAfter)
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut, stdErr)
whatToDoAfter(stdOut)
task = nil
end,
{ '-m', 'query', '--windows'})
task:start()
end
-- and then invoke it like:
getYabaiWindows(function(output)
-- this is where we act on the output from the query
print(output, #output)
end)
~~~
(Since task is defined as local to the getYabaiWindows command, it will go out of scope and be garbage collected, possibly before the query is completed. By referencing it from within the callback to hs.task.new we make it an up-value which keeps it from being collected before completing... once it's completed, the callback has set it to nil -- task = nil -- so it can safely be collected after everything is done)
This is the simplest version that can work with the way both Hammerspoon and yabai are currently written.
I have an idea that might make hs.task:waitUntilExit (or in a new method if we want to keep the operating semantics the same as the NSTask object hs.task is mirroring) work the way you had hoped, but because it means bypassing Hammerspoon's main application loop temporarily for event handling, it's very experimental and will need a lot of testing before I'd release it into the release versions of Hammerspoon... if ever... so the only definitive way I can give you is what I've outlined (and actually just tested myself this time!) above.
I see-- so am I correct to think that my FIFO example performs the same as hs.execute because the main Hammerspoon thread blocks on reading the FIFO (rather than waiting for hs.execute?) Is there a way to pause Hammerspoon's main thread for a few milliseconds without blocking its ability to respond to requests?
Yes, it's "blocking" on the fifo:read('a') line.
And to answer your other question, no... and yes, sorta.
Lua is by default a single application threaded language... there are some various projects out there that attempt to make Lua truly multi-threaded in the sense most people mean when they talk about multi-threaded applications, but their statuses vary and in any case, we're not using any of them.
And while Lua is running code (i.e. in the middle of your function), Hammerspoon can't do anything else (timers, tasks, events, hotkeys, etc.) on the main application thread because that's where Lua is currently processing code.
However, Lua does support coroutines, which is a way for a particular chunk of Lua code to "pause" it's execution and wait for a separate "thread" (poor choice of words, in my opinion, but that's what the Lua docs use) of Lua code to perform some action. This is an extreme over simplification and more detail is beyond the scope of this thread -- if you're really interested check out the Lua docs at https://www.lua.org/docs.html. I've found the third and fourth editions of the "Programming in Lua" book they mention very useful over the years, but the online reference is good, if a little more formal and reference like (as opposed to the books which provide demonstrations), as well.
BUT because of programming choices made early on with Hammerspoon, coroutines are not safe to use in the current release or earlier versions of Hammerspoon if your code uses any functionality that Hammerspoon has added beyond just stock Lua code within the coroutine -- none of our modules work, none of the hooks into hotkeys, timers, tasks, etc. work, etc.
This has been fixed in the current development version of Hammerspoon which means it will be fixed in the next release of Hammerspoon, but I don't have any idea what the time frame on that is. I would assume it's relatively soon as there have been some other additions and bug fixes recently, but there are a couple of pull requests that I think we're still trying to nail down first.
Once that version (or a development version if you want to try your hand at building it yourself -- you'll need XCode. Search the Hammerspoon issues and you should find at least a couple of threads about building it yourself) is installed, the following would work (also just tested this myself):
~~~lua
-- this version can only be used if invoked from within a coroutine
function getYabaiWindowsFromWithinCoroutine()
if not coroutine.isyieldable() then
error("this function cannot be invoked on the main Lua thread")
end
local taskIsDone = false
local output
local task = hs.task.new('/usr/local/bin/yabai',
function(_, stdOut, stdErr)
output = stdOut
taskIsDone = true
end,
{ '-m', 'query', '--windows'})
task:start()
-- this code waits until the flag taskIsDone is set, but requires this function to only
-- be invoked from within a coroutine
while not taskIsDone do
coroutine.applicationYield()
end
return output
end
-- this is just a simplifier -- it wraps our code in a coroutine and starts it
-- in this case, I wanted to make sure that it always creates a new coroutine
-- so it can be invoked anew each time the hotkey below triggers it
function makeActionACoroutine()
-- this makes your code run with a coroutine
coroutine.wrap(function()
-- this is where all of your code needs to go to use the getYabaiWindowsFromWithinCoroutine
-- function and "wait" for it's response
local yOutput = getYabaiWindowsFromWithinCoroutine()
local yJson = hs.json.decode(yOutput)
for i,v in ipairs(yJson) do
print(v.id, v.app, v.title)
end
end)() -- and starts the coroutine running
end
-- I mostly prefer the action when I release the key, unless I'm also soing something with
-- the repeats, hence the initial nil
k = hs.hotkey.bind({"cmd","ctrl","alt"}, "g", nil, makeActionACoroutine)
~~~
What coroutine.applicationYield does is "yield" the coroutine (i.e. pause it and allow other code to do something) and then start a timer that automatically "resumes" the coroutine when the timer fires. In between, the Hammerspoon main application thread is allowed to respond to other events (timers, etc.) that have queued up for action. But as stated in the comments, it requires your code to be fully contained within a coroutine to work properly.
So, yes, it will be possible to pause and give Hammerspoon the time it needs to respond to external queries (a) with the next version of Hammerspoon, and (b) if you write your code appropriately and enclose the bulk of it within a coroutine.
Thanks for the education! I'll just pin to an older yabai until the next HS version is released, then proceed with the coroutine implementation.
@asmagill Just wanted to let you know that I've implemened a coroutine-based solution in the wake of Hammerspoon's new release, and it's working beautifully. Thanks again for your help on this.