cl-async icon indicating copy to clipboard operation
cl-async copied to clipboard

Add piping capabilities to cl-async / uv_pipe_open

Open adolenc opened this issue 8 years ago • 4 comments

Could you please provide an example (or at least point me in the right direction) of using cl-async to write a simplified cat program which just listens for user input on stdin and echoes received data to stdout?

It seems that in libuv this can be done by using uv_pipe_open and providing a file descriptor for input/output streams but I can't seem to be able to figure out how to do it with cl-async.

The reason I need this is that a program (namely neovim) spawns my script as a subprocess and uses stdin/stdout for communication.

adolenc avatar Aug 08 '15 17:08 adolenc

This is an interesting use-case. I think we'd want a pipe.lisp file that extends the streamish object with the custom pipe commands (much like TCP does). Unfortunately, I have a lot on my plate right now and have no time for any new features on cl-async for probably a few months.

In the meantime, one possible solution would be to use cl-async's threading capabilities and have a thread that loops (read-line) in the background, sending any data it gets to the cl-async loop via a notifier.

This seems to work:

echo "andrew" | ccl -e '(format t "hello, ~a~%" (read-line))'

So doing something in a background thread should work equally well.

Hope this helps! Going to leave this open until I get the time to add pipes to cl-async.

orthecreedence avatar Aug 08 '15 17:08 orthecreedence

:+1: Thank you very much! This is a really nice idea and will most likely work fine for the time being.

To answer my original question, implementing (simplified) cat is as easy as:

(as:with-event-loop ()
  (let* ((result nil)
         (notifier (as:make-notifier (lambda ()
                                       (format t "~A~%" result)
                                       (force-output))
                                     :single-shot NIL)))
    (bt:make-thread (lambda ()
                      (loop do (progn (setf result (read-line))
                                      (as:trigger-notifier notifier)))))))

However because parent process doesn't send me lines and I'm more interested in bytes sent, I had to hack together a quick replacement for read-line:

(defun collect-input ()
  "Block thread until data is available on *standard-input* and retrieve it."
  (loop until (listen)
        finally (return (loop while (listen)
                              collect (read-byte *standard-input*)))))

So thanks again, and thank you for this really nice library!

adolenc avatar Aug 08 '15 22:08 adolenc

Cool, glad it worked! This is exactly what I was thinking.

Not sure if this is a big deal or not, but you might want to add a do (sleep 0.01) in the first loop of collect-input:

(loop until (listen)
      do (sleep 0.01)
      finally (return ...))

Otherwise you might burn 100% cpu looping while waiting for input to become available.

orthecreedence avatar Aug 09 '15 00:08 orthecreedence

This can be achieved by polling on fd 0 (stdin) and writing to stdout when appropriate, assuming that neovim handles the io redirection properly for you.

(as:with-event-loop ()
    (as:poll 0 (lambda (event)
                 (assert (equal event '(:readable)))
                 (cffi:with-foreign-object (buf :char 50)
                   (let ((nbytes (c-read 0 buf 50)))
                     (format t "~d bytes recieved: ~A~&"
                             nbytes
                             (cffi:foreign-string-to-lisp buf)))))
             :poll-for '(:readable)))

Plisp avatar Nov 02 '19 05:11 Plisp