Client created in same function not found by eval

Hi all, I have the following wrapper method to create a new split then set up tools and jump clients before calling grep, make etc:

define-command -docstring "split [<commands>]: split tmux horizontally" \
split -params .. -command-completion %{
    tmux-terminal-vertical kak -c %val{session} -e "%arg{@}"
}

define-command tool -params .. -command-completion -docstring "create tool split then run command" %{
    evaluate-commands %sh{
        if [ "$kak_client" != "main" ]; then
            echo "rename-client main; set global jumpclient main"
        fi
        if [ "${kak_quoted_client_list#*'tools'}" = "$kak_quoted_client_list" ]; then
            echo "split rename-client tools; set global toolsclient tools"
        fi
    }
    try %{
        evaluate-commands -draft %arg{@}
    } catch %{
        evaluate-commands -client %opt{toolsclient} "quit"
    }
}

When I call an invalid command like :tool aha that is supposed to fail, the new split opens but the catch clause cannot execute because eval cannot find the client named tools, which was just created. Am I missing something basic above, or is this a limitation I should work around? Also any suggestions on a better alternative to the tool function is welcome. Thanks!

Hi brave, I don’t have the answer but this discuss.kakoune thread might point you in the right direction or NOT. Branching on a boolean option -- without calling the shell. Feel free to just reply NOT or wait for someone smarter :wink:.

Thought I better come back and clarify the NOT meaning and purpose in the above paragraph.

Sorry, I tried to reply before via email, but it looks like my message got caught in a spam trap somewhere.

This smells like a race-condition. The split command tells the kernel to launch a tmux client, which does some processing and then tells the kernel to connect to the tmux server, which does some processing and then tells the kernel to launch a Kakoune client, which does some processing and then tells the kernel to connect to the Kakoune server, and eventually after all that long chain of consequences occurs the Kakoune client renames itself “tools”.

Meanwhile, back in the original Kakoune instance, Kakoune tries to run an invalid command, then tries to quit the “tools” client. There’s a pretty good chance that Kakoune will detect the failure and try to quit the “tools” client before the “tools” client exists, or at least before it manages to register itself with the Kakoune server.

As a workaround, instead of just launching a tools client and letting it sit there, you might consider making it execute the intended commands (with eval -client main) once it’s set itself up, and quitting itself if there’s an error. That way there’s no chance the “tools” client won’t be ready in time, because the commands will only be executed once the “tools” client is ready.

Thanks both for your replies. @Screwtapello if I understood you correctly, the execution of operations are not exactly synchronous (waiting until each step is finished before executing the next), when there is client creation involved. So when I tell Kakoune to create a new client then run some other command, it is possible the client is not completely initialized yet.

With your suggestion I made the following change and it seems to work without issue now, cheers!

# set tools/jumpclient for tools
define-command tool -params .. -command-completion -docstring "create tool split and run command" %{
    evaluate-commands %sh{
        if [ "${kak_client_list#*main}" = "$kak_client_list" ]; then  # if main client doesn't exist, set current as main
            printf "%s\n" "rename-client main; set global jumpclient main"
        fi
        if [ "${kak_client_list#*tools}" = "$kak_client_list" ]; then  # create tools client and run
            printf "%s\n" \
            "set global toolsclient tools
            split %{
                rename-client tools
                try %{
                    evaluate-commands -client main $*
                } catch %{
                    echo -debug %val{error}
                    quit
                }
            }"
        else  # tools client exists, just run command
            printf "%s\n" \
            "try %{
                evaluate-commands -client main $*
            } catch %{
                echo -debug %val{error}
                evaluate-commands -client tools quit
            }"
        fi
    }
}
1 Like

Race conditions is an outstanding issue for me. Can either of you point me in the direction of some literature that I can read about checking / running processes? I do prefer a bit more depth than stackoverflow usually provides. Any help you can give me is always appreciated.

I currently use the command sleep n which is always inaccurate, the script below is my naive solution and never works correctly.

polar-bookshelf.sh, disclaimer this is not kakoune related.

Glad it worked out bravekarma. Bye :wave:

Thank you for giving me something to go on, appreciate the succinctness.

A race condition is when two components of a system are acting independently, and try to communicate without being sure that the other end is ready. It’s pretty common, and can be very difficult to debug.

When the components we’re talking about are POSIX processes, there’s not a lot we can do — POSIX doesn’t provide ways (in the general case) for one process to reliably track the state of another. What you do have is:

  • If the other process is one you’ve directly spawned, you can use wait(2) to be sure it’s exited. This doesn’t necessarily help if you need to communicate with the other process, but if the child is (for example) writing to a file, you can wait(2) for the child to exit, and if the exit status is 0 you can assume the file is ready for you to read.
  • If one process is long-lived and the other is short-lived, rather than have the long-lived process guess when the short-lived one is ready, just make the short-lived process initiate communication. That’s the gist of my suggestion above.
  • FIFOs make good rendezvous points because in the default configuration, when one process grabs one end of a FIFO, the kernel will freeze it until another process grabs the other end. However, in the default configuration when the reader goes away the kernel will try to kill the writer, so if that’s not what you want, you might need to ignore SIGPIPE or write in non-blocking mode or something.

As you point out, sleep is not a reliable solution, because you can never be sure you’ve waited long enough (what if the machine is in swapping hell and takes hours to launch the process you want to launch?). Unfortunately, for the example you give I don’t have a better solution: it seems to be waiting for a GUI app to get itself ready to accept input, and there’s no good way to tell when that’s complete. Perhaps you could file an issue for Polar Bookshelf to emit some kind of signal (read a FIFO, create a new file on disk, etc.) when it’s ready, and then your script can check for that.

While researching the above I have come across this work which I think community may enjoy. As it’s on topic with *nix.

Wheeler, D 2015, Secure Programming HOWTO, Creating Secure Software, v3.72 edn, viewed 25 May 2020, https://dwheeler.com/secure-programs/

Abstract:
This book provides a set of design and implementation guidelines for writing secure programs. Such programs include application programs used as viewers of remote data, web applications (including CGI scripts), network servers, and setuid/setgid programs. Specific guidelines for C, C++, Java, Perl, PHP, Python, Tcl, and Ada95 are included. It especially covers Linux and Unix based systems, but much of its material applies to any system. For a current version of the book, see http://www.dwheeler.com/secure-programs

My journey continues. Bye :wave:


Is referenced in Wheeler and was a little hard to find so I have included it here for ease.

Zalewski, M 2001, Delivering Signals for Fun and Profit, Understanding, exploiting and preventing signal-handling related vulnerabilities, BindView Corporation, viewed 25 May 2020, https://lcamtuf.coredump.cx/signals.txt

Another way to avoid a race condition is to register a ClientCreate hook before creating the new client. The hook will only execute after the client has initialized.

hook -once global ClientCreate .* %{
    rename-client out
    evaluate-commands -client out %{
        echo "I am out"
    }
}
new

After the client is initialized, CreateClient is triggered (with an arbitrary client name chosen by Kakoune, hence .* to match any name). Only then is the client renamed and commands are run inside it.

2 Likes