asio service handler for stdin keypress
Asked Answered
S

4

7

I have adapted step 3 of the Boost asio tutorial to run forever, and display "tick" and "tock" once per second instead of the counter:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

void print(const boost::system::error_code& /*e*/,
    boost::asio::deadline_timer* t, int* count)
{
    if( !((*count) % 2) )
        std::cout << "tick\n";
    else
        std::cout << "tock\n";

    ++(*count);

    t->expires_at(t->expires_at() + boost::posix_time::seconds(1));
    t->async_wait(boost::bind(print,
          boost::asio::placeholders::error, t, count));
}

int main()
{
  boost::asio::io_service io;

  int count = 0;
  boost::asio::deadline_timer t(io, boost::posix_time::seconds(1));
  t.async_wait(boost::bind(print,
        boost::asio::placeholders::error, &t, &count));

  io.run();

  std::cout << "Final count is " << count << "\n";

  return 0;
}

Now I want to asynchronously be able to handle a keypress on stdin. Is there an io_service handler I can use to respond to keypresses, without blocking sleeps or waits?

For example, I'd like to be able to implement a handler function similar to:

void handle_keypress(const boost::error_code&,
    char c)
{
    std::cout << "Tap '" << c << "'\n";
}

And I would expect my invocation of this handler to be something along the lines of:

  char c = 0;
  boost::asio::stdin_receiver sr(io);
  st.async_wait(boost::bind(handle_keypress, boost::asio::placeholders::error, &c));

  io.run();

Is this something I can do with asio, either by using a builtin service handler, or writing my own?

EDIT, ELABORATION:

I have seen this question, but the linked-to code in the accpeted answer simply does this in main:

 while (std::cin.getline(

The application I'm writing isn't this simple tick-tock-tap gizmo I've outlined above, but will be a multicast server. Several worker threads will be sending packets to multicast groups, responding to messages from the main thread, and sending messages back to the main thread. The application, in turn, will be "driven" by input from the stdin -- for example, when the user presses the "P" key, multicast broadcast will be paused, and when the hit "Q" the whole thing will shut down. In the main thread, all I'll do in response to these inputs is send messages to the worker threads.

The while loop above won't work in my scenario because while it's waiting for stdin input from the user, the main thread will not be able to process messages coming in from the worker threads. Some of those messages from the worker thread will generate output to stdout.

Stomach answered 7/6, 2012 at 15:2 Comment(4)
Have you looked at the posix_chat_client example that is described as "The following POSIX-specific chat client demonstrates how to use the posix::stream_descriptor class to perform console input and output." boost.org/doc/libs/1_49_0/doc/html/boost_asio/examples.htmlContrarily
@Ralf: Thanks for your comment. I was just editing my post about that when you were entering your comment. I did, and unless I mis-understood the example, I don't think it does what I want to do. Please see my edit above.Stomach
Not sure if I understand you correctly, but in the example, boost::asio_service::run is called in a different thread, hence asio I/O will still be processed?Contrarily
@Ralf: run is called in a different thread, but in the non-POSIX example the cin.getline is in the main thread. I want a solution that works in both Windows and Linux, I should have mentioned, and the POSIX version of the chat client is #ifdef-ed away.Stomach
R
2

There is always the option of handling stdin in a separate thread and posting any keypresses to your main event loop via io_service::dispatch

This function is used to ask the io_service to execute the given handler.

The io_service guarantees that the handler will only be called in a thread in which the run(), run_one(), poll() or poll_one() member functions is currently being invoked.

Rishi answered 7/6, 2012 at 21:28 Comment(2)
+1: Yes, there is this option, and from what I've been reading everywhere this would seem to be about the only option -- at least, while using boost. However, this brings a new problem -- how do you tell the stdin-handling thread that it's time to die? It's blocked in a read call. I could send a "poison pill" down the stdin, but I would like a less hacky way.Stomach
Im my final solution I ended up abandoning asio as it simply doesn't have the facilities I need. Regardless, if I were to use asio this is how I would have to do it.Stomach
T
6

I don't believe the posix chat client uses a while loop or invokes std::getline, which is the sample code you linked to in my previous answer. Perhaps you are referring to another example? In any case, you don't need to use io_service::dispatch or even a separate thread. The built-in facilities of a stream descriptor work just fine here. See my previous answer to a similar question: Use a posix::stream_descriptor and assign STDIN_FILENO to it. Use async_read and handle the requests in the read handlers.

I've modified your sample code with one way to accomplish this

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

void print(const boost::system::error_code& /*e*/,
    boost::asio::deadline_timer* t, int* count)
{
    if( !((*count) % 2) )
        std::cout << "tick\n";
    else
        std::cout << "tock\n";

    ++(*count);

    t->expires_at(t->expires_at() + boost::posix_time::seconds(1));
    t->async_wait(boost::bind(print,
          boost::asio::placeholders::error, t, count));
}

class Input : public boost::enable_shared_from_this<Input>
{
public:
    typedef boost::shared_ptr<Input> Ptr;

public:
    static void create(
            boost::asio::io_service& io_service
            )
    {
        Ptr input(
                new Input( io_service )
                );
        input->read();
    }

private:
    explicit Input(
            boost::asio::io_service& io_service
         ) :
        _input( io_service )
    {
        _input.assign( STDIN_FILENO );
    }

    void read()
    {
        boost::asio::async_read(
                _input,
                boost::asio::buffer( &_command, sizeof(_command) ),
                boost::bind(
                    &Input::read_handler,
                    shared_from_this(),
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred
                    )
                );
    }

    void read_handler(
            const boost::system::error_code& error,
            const size_t bytes_transferred
            )
    {
        if ( error ) {
            std::cerr << "read error: " << boost::system::system_error(error).what() << std::endl;
            return;
        }

        if ( _command != '\n' ) {
            std::cout << "command: " << _command << std::endl;
        }

        this->read();
    }

private:
    boost::asio::posix::stream_descriptor _input;
    char _command;
};

int main()
{
  boost::asio::io_service io;

  int count = 0;
  boost::asio::deadline_timer t(io, boost::posix_time::seconds(1));
  t.async_wait(boost::bind(print,
        boost::asio::placeholders::error, &t, &count));

  Input::create( io);

  io.run();

  std::cout << "Final count is " << count << "\n";

  return 0;
}

compile, link, and run

samm:stackoverflow samm$ g++ -I /opt/local/include stdin.cc -L /opt/local/lib -lboost_system -Wl,-rpath,/opt/local/lib
samm:stackoverflow samm$ echo "hello world" | ./a.out
command: h
command: e
command: l
command: l
command: o
command:  
command: w
command: o
command: r
command: l
command: d
read error: End of file
tick
tock
tick
tock
tick
tock
tick
tock
tick
tock
^C
samm:stackoverflow samm$
Taille answered 12/6, 2012 at 3:12 Comment(2)
@cmeerw I did not see the Windows requirement specified in the question, after reading comments in the other answer I see that now. In any case, the stream_descriptor is an elegant way to solve John's question on platforms supporting it.Taille
stream_descriptor requires hitting enter, isn't it?Aylsworth
R
2

There is always the option of handling stdin in a separate thread and posting any keypresses to your main event loop via io_service::dispatch

This function is used to ask the io_service to execute the given handler.

The io_service guarantees that the handler will only be called in a thread in which the run(), run_one(), poll() or poll_one() member functions is currently being invoked.

Rishi answered 7/6, 2012 at 21:28 Comment(2)
+1: Yes, there is this option, and from what I've been reading everywhere this would seem to be about the only option -- at least, while using boost. However, this brings a new problem -- how do you tell the stdin-handling thread that it's time to die? It's blocked in a read call. I could send a "poison pill" down the stdin, but I would like a less hacky way.Stomach
Im my final solution I ended up abandoning asio as it simply doesn't have the facilities I need. Regardless, if I were to use asio this is how I would have to do it.Stomach
P
2

Under Linux the key to getting STDIN keypress events lies in the terminal configuration. You can unset the ICANON flag via tcsetattr() and then you will get every single key pressed event. Here is an example program, which demonstrates that. Note that the buffer is larger than one, so we can get more characters at once as well, if they are availabe (for example when you copy&paste).

#include <boost/asio.hpp>
#include <iostream>

int main()
{
    boost::asio::io_service ioservice;
    boost::asio::posix::stream_descriptor in(ioservice, STDIN_FILENO);
    char buf[10];

    struct termios term_old;
    tcgetattr(STDIN_FILENO, &term_old);
    struct termios newt = term_old;

    newt.c_lflag &= ~(ICANON); // don't wait until EOL
    newt.c_lflag &= ~(ECHO);   // don't echo

    tcsetattr(STDIN_FILENO, TCSANOW, &newt);

    std::function<void(boost::system::error_code, size_t)> read_handler
        = [&](boost::system::error_code ec, size_t len) {
              if (ec) {
                  std::cerr << "exit: " << ec.message() << std::endl;
              } else {
                  buf[len] = '\0';
                  std::cout << "in: " << buf << std::endl;
                  in.async_read_some(boost::asio::buffer(buf), read_handler);
              }
          };

    in.async_read_some(boost::asio::buffer(buf), read_handler);

    ioservice.run();

    tcsetattr(STDIN_FILENO, TCSANOW, &term_old);
}
Punchy answered 29/10, 2021 at 13:24 Comment(0)
K
1

Here's a complete and simple example using a thread (Note, this is using the asio library, but it should be straightforward to convert to boost::asio):

#include <asio.hpp>
#include <iostream>

void start_timer(asio::steady_timer& timer, asio::io_context& timer_io)
{
    timer.expires_from_now(asio::chrono::seconds(1));
    timer.async_wait(
        [&timer, &timer_io](const std::error_code& ec)
        {
            if (!ec)
            {
                // get the time since epoch in seconds
                auto time =
                    std::chrono::duration_cast<std::chrono::seconds>(
                        std::chrono::system_clock::now().time_since_epoch())
                        .count();
                std::cout << (time % 2 == 0 ? "tick" : "tock") << std::endl;
                start_timer(
                    timer,
                    timer_io); // Restart the timer for the next tick/tock
            }
        });
}

int main()
{
    asio::io_context io;
    asio::io_context timer_io;
    asio::steady_timer timer(timer_io);
    std::thread timer_thread(
        [&]()
        {
            start_timer(timer, timer_io);
            timer_io.run();
        });

    // Start an asynchronous operation to wait for user input
    asio::post(io,
            [&io]()
            {
                char input[1];
                std::cout << "Enter key to exit!" << std::endl;
                std::cin.getline(input, 1);
                io.stop();
            });

    // Run the IO context until user input handling is complete
    io.run();

    // Once the user io has run to completion, stop the timer io
    timer_io.stop();

    // Clean up the timer thread
    timer_thread.join();

    std::cout << "Enter key pressed. Exiting..." << std::endl;
    return 0;
}
Kunstlied answered 28/8, 2023 at 7:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.