1

I`m trying to use a web socket client that connects to a server using Boost library. The situation is that the server sometimes sends pre-determined amounts of JSON messages, but sometimes more.

From stack overflow I have a solution posted by @sehe, which can be found here. This works well for me if I know for sure the amount of messages sent back is 1,2,3, etc.

However it does not work well if:

  • You specify less amount of messages received; you wont get the message "now" and it will be appended in the next read
  • You specify more than the messages expected; it will get stuck waiting for messages

I have done a little digging and tested the async example client from the Boost website. It works "well", for 1 message. Using that example inside a thread or timer will trigger the assert from Boost.

The ideal solution for me would be what @sehe posted, short, simple; but it should read "all" the messages sent back. I realise this can be done only if you "know" when the message stream "ends", but with my lack of experience in using Boost and web sockets in C++ I am lost.

Please advise what would be the solution for this purpose. To re-iterate:

  • Send command
  • Wait for response; read all response (even if 10 JSON objects)

Many thanks

sehe
  • 374,641
  • 47
  • 450
  • 633
Mecanik
  • 1,539
  • 1
  • 20
  • 50
  • How long will you wait? How would you know when the responses are "done"? (Websocket is message-oriented by definition). It feels like you're simply looking for full-duplex IO (indepent receive/writes) which can be done trivially both sync and async. – sehe Jan 19 '22 at 10:27
  • @sehe I understand what you are saying, been thinking about this. But because of the lack of knowledge and experience with this, I do not want to talk nonsense. I believe the best example is this https://chromedevtools.github.io/devtools-protocol/. Some commands return pre-defined messages back, so that's fine. But if you send a "navigate" command... it will fill you up with messages. – Mecanik Jan 19 '22 at 10:44
  • Again, how do *you* want to handle that? It seems you really need full-duplex, and then you can relate responses to requests later if applicable? (I'm not going to study a vast protocol suite just to see what you need) – sehe Jan 19 '22 at 10:49
  • Found this on Command Ordering https://docs.google.com/document/d/1rlqcp8nk-ZQvldNJWdbaMbwfDbJoOXvahPCDoPGOwhQ/edit#heading=h.6g68k6u07nq4 – sehe Jan 19 '22 at 11:26
  • @sehe Sorry for late reply. I`m not sure what you mean by "how I handle that", again, not much experience. What I am doing now (using your class), is send + receive and parse several commands, one after another. I would need the response "asap", since I need to access data before the next command. Perhaps... possible "chain" somehow these commands to execute one after another? – Mecanik Jan 19 '22 at 14:15
  • @sehe If you want I could give you a very easy 2 minute way of seeing these responses? Perhaps in chat? – Mecanik Jan 19 '22 at 14:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/241215/discussion-between-sehe-and-mecanik). – sehe Jan 19 '22 at 15:23

1 Answers1

1

In response to the comments/chat I have cooked up¹ an example of a straight-forward translation of the example from e.g. https://github.com/aslushnikov/getting-started-with-cdp#targets--sessions into C++ using Beast.

Note that it uses the command IDs to correlate responses to requests. Note as well that these are session-specific, so if you must support multiple sessions, you will need to account for that.

#1: Callback Style

#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/signals2.hpp>
#include <iostream>
#include <deque>
#include <ranges>
#include <boost/json.hpp>
//#include <boost/json/src.hpp> // for header-only

namespace json      = boost::json;
namespace net       = boost::asio;
namespace beast     = boost::beast;
namespace websocket = beast::websocket;

namespace r = std::ranges;

static std::ostream debug(/*nullptr*/ std::cerr.rdbuf());

using namespace std::chrono_literals;
using boost::signals2::scoped_connection;
using boost::system::error_code;
using net::ip::tcp;

// Sends a WebSocket message and prints the response
class CWebSocket_Sync {
    websocket::stream<tcp::socket> ws_;

  public:
    using executor_type = net::any_io_executor;
    executor_type get_executor() { return ws_.get_executor(); }

    // Resolver and socket require an io_context
    explicit CWebSocket_Sync(executor_type ex) : ws_(make_strand(ex)) {}

    // call backs are on the strand, not on the main thread
    boost::signals2::signal<void(json::object const&)> onMessage;

    // public functions not assumed to be on the strand
    void Connect(std::string const& host, std::string const& port, std::string const& path)
    {
        post(get_executor(), [=, this] {
            tcp::resolver resolver_(get_executor());

            // TODO async_connect prevents potential blocking wait
            // TODO async_handshake (idem)
            auto ep = net::connect(ws_.next_layer(), //
                                   resolver_.resolve(host, port));
            ws_.handshake(host + ':' + std::to_string(ep.port()), path);
            do_receive_loop();
        });
    }

    void ServerCommand(json::object const& cmd)
    {
        post(get_executor(), [text = serialize(cmd), this] {
            outbox_.push_back(text);

            if (outbox_.size() == 1) // not already sending?
                do_send_loop();
        });
    }

    void CloseConnection() {
        post(get_executor(), [this] {
            ws_.next_layer().cancel();
            ws_.async_close(websocket::close_code::normal, [](error_code ec) {
                debug << "CloseConnection (" << ec.message() << ")" << std::endl;
            });
        });
    }

  private:
    // do_XXXX functions assumed to be on the strand
    beast::flat_buffer inbox_;

    void do_receive_loop() {
        debug << "do_receive_loop..." << std::endl;

        ws_.async_read(inbox_, [this](error_code ec, size_t n) {
            debug << "Received " << n << " bytes (" << ec.message() << ")" << std::endl;

            if (!ec) {
                auto text   = inbox_.cdata();
                auto parsed = json::parse(
                    {buffer_cast<char const*>(text), text.size()}, ec);
                inbox_.clear();

                if (!ec) {
                    assert(parsed.is_object());
                    onMessage(parsed.as_object()); // exceptions will blow up
                    do_receive_loop();
                } else {
                    debug << "Ignore failed parse (" << ec.message() << ")" << std::endl;
                }
            }

        });
    }

    std::deque<std::string> outbox_;

    void do_send_loop() {
        debug << "do_send_loop " << outbox_.size() << std::endl;
        if (outbox_.empty())
            return;

        ws_.async_write( //
            net::buffer(outbox_.front()), [this](error_code ec, size_t n) {
                debug << "Sent " << n << " bytes (" << ec.message() << ")" << std::endl;

                if (!ec) {
                    outbox_.pop_front();
                    do_send_loop();
                }
            });
    }
};

int main()
{
    net::thread_pool ioc(1);

    CWebSocket_Sync client(ioc.get_executor());
    client.Connect("localhost", "9222", "/devtools/browser/bb8efece-b445-42d0-a4cc-349fccd8514d");

    auto trace = client.onMessage.connect([&](json::object const& obj) {
        debug << "Received " << obj << std::endl;
    });

    unsigned id = 1; // TODO make per session
    scoped_connection sub = client.onMessage.connect([&](json::object const& obj) {
        if ((obj.contains("id") && obj.at("id") == 1)) {
            auto& infos = obj.at("result").at("targetInfos").as_array();
            if (auto pageTarget = r::find_if(infos,
                    [](auto& info) { return info.at("type") == "page"; })) //
            {
                std::cout << "pageTarget " << *pageTarget << std::endl;

                sub = client.onMessage.connect([&](json::object const& obj) {
                    // idea:
                    // if(obj.contains("method") && obj.at("method") == "Target.attachedToTarget"))
                    if (obj.contains("id") && obj.at("id") == 2) {
                        auto sessionId = value_to<std::string>(obj.at("result").at("sessionId"));
                        std::cout << "sessionId: " << sessionId << std::endl;

                        sub.release(); // stop expecting a specific response

                        client.ServerCommand({
                            {"sessionId", sessionId},
                            {"id", 1}, // IDs are independent between sessions
                            {"method", "Page.navigate"},
                            {"params", json::object{
                                 {"url", "https://stackoverflow.com/q/70768742/85371"},
                             }},
                        });
                    }
                });

                client.ServerCommand(
                    {{"id", id++},
                     {"method", "Target.attachToTarget"},
                     {
                         "params",
                         json::object{
                             {"targetId", pageTarget->at("targetId")},
                             {"flatten", true},
                         },
                     }});
            }
        }
    });

    client.ServerCommand({
        {"id", id++},
        {"method", "Target.getTargets"},
    });

    std::this_thread::sleep_for(5s);
    client.CloseConnection();

    ioc.join();
}

When testing (I hardcoded the websocket URL for now);

enter image description here

The complete output is:

do_receive_loop...
do_send_loop 1
Sent 37 bytes (Success)
do_send_loop 0
Received 10138 bytes (Success)
Received {"id":1,"result":{"targetInfos":[{"targetId":"53AC5A92902F306C626CF3B3A2BB1878","type":"page","title":"Google","url":"https://www.google.com/","attached":false,"canAccessOpener":false,"browserContextId":"15E97D88D0D1417314CBCB24D4A0FABA"},{"targetId":"D945FE9AC3EBF060805A90097DF2D7EF","type":"page","title":"(1) WhatsApp","url":"https://web.whatsapp.com/","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"6DBC2EDCADF891A4A68FA9A878AAA574","type":"page","title":"aslushnikov/getting-started-with-cdp: Getting Started With Chrome DevTools Protocol","url":"https://github.com/aslushnikov/getting-started-with-cdp#targets--sessions","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"35BE8DA1EE5A0F51EDEF9AA71738968C","type":"background_page","title":"Gnome-shell-integratie","url":"chrome-extension://gphhapmejobijbbhgpjhcjognlahblep/extension.html","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"477A0D3805F436D95C9D6DC0760862C1","type":"background_page","title":"uBlock Origin","url":"chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm/background.html","attached":false,"canAccessOpener":false,"browserContextId":"15E97D88D0D1417314CBCB24D4A0FABA"},{"targetId":"B1371BC4FA5117900C2ABF28C69E3098","type":"page","title":"On Software and Languages: Holy cow, I wrote a book!","url":"http://ib-krajewski.blogspot.com/2019/02/holy-cow-i-wrote-book.html","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"1F3A58D579C18DDD819EF46EBBB0AD4C","type":"page","title":"c++ - Boost Beast Websocket - Send and Read until no more data - Stack Overflow","url":"https://stackoverflow.com/questions/70768742/boost-beast-websocket-send-and-read-until-no-more-data","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"A89EBECFD804FD9D4FF899274CB1E4C5","type":"background_page","title":"Dark Reader","url":"chrome-extension://eimadpbcbfnmbkopoojfekhnkhdbieeh/background/index.html","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"},{"targetId":"9612E681CCF4E4E47D400B0849FA05E6","type":"background_page","title":"uBlock Origin","url":"chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm/background.html","attached":false,"canAccessOpener":false,"browserContextId":"9806733E4CD80888448B20DA32A515F6"}]}}
pageTarget {"targetId":"53AC5A92902F306C626CF3B3A2BB1878","type":"page","title":"Google","url":"https://www.google.com/","attached":false,"canAccessOpener":false,"browserContextId":"15E97D88D0D1417314CBCB24D4A0FABA"}
do_receive_loop...
do_send_loop 1
Sent 113 bytes (Success)
do_send_loop 0
Received 339 bytes (Success)
Received {"method":"Target.attachedToTarget","params":{"sessionId":"29AD9FFD2EAE70BAF10076A9E05DD000","targetInfo":{"targetId":"53AC5A92902F306C626CF3B3A2BB1878","type":"page","title":"Google","url":"https://www.google.com/","attached":true,"canAccessOpener":false,"browserContextId":"15E97D88D0D1417314CBCB24D4A0FABA"},"waitingForDebugger":false}}
do_receive_loop...
Received 66 bytes (Success)
Received {"id":2,"result":{"sessionId":"29AD9FFD2EAE70BAF10076A9E05DD000"}}
sessionId: 29AD9FFD2EAE70BAF10076A9E05DD000
do_receive_loop...
do_send_loop 1
Sent 142 bytes (Success)
do_send_loop 0
Received 157 bytes (Success)
Received {"id":1,"result":{"frameId":"53AC5A92902F306C626CF3B3A2BB1878","loaderId":"A3680FBE84DEBDA3444FFA6CD7C5A5A5"},"sessionId":"29AD9FFD2EAE70BAF10076A9E05DD000"}
do_receive_loop...
Received 0 bytes (Operation canceled)
CloseConnection (Operation canceled)

#2: Promises/Future Style

I created a Request method that returns a future like the nodejs example:

std::future<json::object> Request(json::object const& cmd)
{
    auto fut = Expect([id = msgId(cmd)](json::object const& resp) {
        return msgId(resp) == id;
    });

    Send(cmd);

    return fut;
}

Note how it got a bit more elegant with the addition of the msgId extraction helper:

static json::object msgId(json::object const& message) {
    return filtered(message, {"id", "sessionId"}); // TODO more ?
};

This neatly facilitates multi-session responses where the "id" need not be unique across different "sessionId"s. The condition stays a simple if (msgId(msg) == id).

It also uses Send and Expect as building blocks:

void Send(json::object const& cmd)
{
    post(get_executor(), [text = serialize(cmd), this] {
        outbox_.push_back(text);

        if (outbox_.size() == 1) // not already sending?
            do_send_loop();
    });
}

template <typename F> std::future<json::object> Expect(F&& pred)
{
    struct State {
        boost::signals2::connection _subscription;
        std::promise<json::object>  _promise;
    };

    auto state = std::make_shared<State>();

    state->_subscription = onMessage.connect( //
        [=, pred = std::forward<F>(pred)](json::object const& msg) {
            if (pred(msg)) {
                state->_promise.set_value(msg);
                state->_subscription.disconnect();
            }
        });

    return state->_promise.get_future();
}

Now the main program can be written less backwards:

auto targets = client.Request({
    {"id", id++},
    {"method", "Target.getTargets"},
}).get().at("result").at("targetInfos");

auto pageTarget = r::find_if(targets.as_array(), [](auto& info) {
    return info.at("type") == "page";
});

if (!pageTarget) {
    std::cerr << "No page target\n";
    return 0;
}

std::cout << "pageTarget " << *pageTarget << std::endl;
auto sessionId = client.Request(
        {{"id", id++},
           {"method", "Target.attachToTarget"},
           {"params", json::object{
               {"targetId", pageTarget->at("targetId")},
               {"flatten", true},
           },
        }})
        .get().at("result").at("sessionId");

std::cout << "sessionId: " << sessionId << std::endl;

auto response = client.Request({
        {"sessionId", sessionId},
        {"id", 1}, // IDs are independent between sessions
        {"method", "Page.navigate"},
        {"params", json::object{
             {"url", "https://stackoverflow.com/q/70768742/85371"},
         }},
    }) .get();

std::cout << "Navigation response: " << response << std::endl;

Which leads to output like:

 -- trace {"id":1,"result":{"targetInfos":[{"targetId":"35BE8DA1EE5A0F51EDEF9AA71738968C","type":"background_page","title":"Gnom....
pageTarget {"targetId":"1F3A58D579C18DDD819EF46EBBB0AD4C","type":"page","title":"c++ - Boost Beast Websocket - Send and Read unt....
 -- trace {"method":"Target.attachedToTarget","params":{"sessionId":"58931793102C2A5576E4D5D6CDC3D601","targetInfo":{"targetId":....
 -- trace {"id":2,"result":{"sessionId":"58931793102C2A5576E4D5D6CDC3D601"}}
sessionId: "58931793102C2A5576E4D5D6CDC3D601"
 -- trace {"id":1,"result":{"frameId":"1F3A58D579C18DDD819EF46EBBB0AD4C","loaderId":"9E70C5AAF0B5A503BA2770BB73A4FEC3"},"session....
Navigation response: {"id":1,"result":{"frameId":"1F3A58D579C18DDD819EF46EBBB0AD4C","loaderId":"9E70C5AAF0B5A503BA2770BB73A4FEC3....

Comment After Care:

I would have one last question if you do not mind? Can I use somehow std::future<T>::wait_until so I can find out if the page was loaded completely? (for example checking for Network.loadingFinished object)?

Sure, just code it:

{
    std::promise<void> promise;
    scoped_connection  sub =
        client.onMessage.connect([&](json::object const& msg) {
            if (auto m = msg.if_contains("method"); *m == "Network.loadingFinished")
                promise.set_value();
        });

    auto loadingFinished = promise.get_future();
    loadingFinished.wait(); // OR:
    loadingFinished.wait_for(5s); // OR:
    loadingFinished.wait_until(std::chrono::steady_clock::now() + 1min);
}

To also have the message:

{
    std::promise<json::object> promise;
    scoped_connection  sub =
        client.onMessage.connect([&](json::object const& msg) {
            if (auto m = msg.if_contains("method"); *m == "Network.loadingFinished")
                promise.set_value(msg);
        });

    auto message = promise.get_future().get();;
}

Of course you could/should consider encapsulating in a class method again.

UPDATE - I have since refactored the original futures code to use these as building blocks (Expect, Send together make Request)

Now you can just

auto loadingFinished = client.Expect(isMethod("Network.loadingFinished")).get();
std::cout << "Got: " << loadingFinished << "\n";

Of course, assuming a tiny helper like:

auto isMethod = [](auto value) {
    return [value](json::object const& msg) {
        auto m = msg.if_contains("method");
        return m && *m == value;
    };
};

As a bonus, to monitor continuously for specific messages:

enum ActionResult { ContinueMonitoring, StopMonitoring };

template <typename A, typename F>
auto Monitor(A action, F&& filter = [](auto&&) noexcept { return true; })
{
    struct State {
        boost::signals2::connection _subscription;
        std::promise<json::object>  _promise;
    };

    auto state = std::make_shared<State>();
    auto stop  = [state] { state->_subscription.disconnect(); };

    state->_subscription = onMessage.connect( //
        [=, filter = std::forward<F>(filter)](json::object const& msg) {
            if (filter(msg) && StopMonitoring == action(msg))
                stop();
        });

    return stop; // gives the caller an "external" way to stop the monitor
}

A contrived example of usage:

// monitor until 3 messages having an id divisable by 7 have been received
std::atomic_int count = 0;

auto stopMonitor = client.Monitor(
    [&count](json::object const& msg) {
        std::cout << "Divisable by 7: " << msg << "\n";
        return ++count >= 3 ? CDPClient::StopMonitoring
                            : CDPClient::ContinueMonitoring;
    },
    [](json::object const& msg) {
        auto id = msg.if_contains("id");
        return id && (0 == id->as_int64() % 7);
    });

std::this_thread::sleep_for(5s);

stopMonitor(); // even if 3 messages had not been reached, stop the monitor

std::cout << count << " messages having an id divisable by 7 had been received in 5s\n";

Full Listing (of the Futures Version)

Sadly Exceeds Compiler Explorer Limits:

#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/json.hpp>
//#include <boost/json/src.hpp> // for header-only
#include <boost/signals2.hpp>
#include <deque>
#include <iostream>
#include <ranges>

namespace json      = boost::json;
namespace net       = boost::asio;
namespace beast     = boost::beast;
namespace websocket = beast::websocket;

namespace r = std::ranges;

static std::ostream debug(nullptr); // std::cerr.rdbuf()

static const auto filtered(json::object const&                      obj,
        std::initializer_list<json::string_view> props)
{
    boost::json::object result;
    for (auto prop : props)
        if (auto const* v = obj.if_contains(prop))
            result[prop] = *v;
    return result;
}

using namespace std::chrono_literals;
using boost::signals2::scoped_connection;
using boost::system::error_code;
using net::ip::tcp;

// Sends a WebSocket message and prints the response
class CDPClient {
    websocket::stream<tcp::socket> ws_;

    public:
    using executor_type = net::any_io_executor;
    executor_type get_executor() { return ws_.get_executor(); }

    // Resolver and socket require an io_context
    explicit CDPClient(executor_type ex) : ws_(make_strand(ex)) {}

    // call backs are on the strand, not on the main thread
    boost::signals2::signal<void(json::object const&)> onMessage;

    // public functions not assumed to be on the strand
    void Connect(std::string const& host, std::string const& port, std::string const& path)
    {
        post(get_executor(), [=, this] {
                tcp::resolver resolver_(get_executor());

                // TODO async_connect prevents potential blocking wait
                // TODO async_handshake (idem)
                auto ep = net::connect(ws_.next_layer(), //
                                       resolver_.resolve(host, port));
                ws_.handshake(host + ':' + std::to_string(ep.port()), path);
                do_receive_loop();
            });
    }

    void Send(json::object const& cmd)
    {
        post(get_executor(), [text = serialize(cmd), this] {
            outbox_.push_back(text);

            if (outbox_.size() == 1) // not already sending?
                do_send_loop();
        });
    }

    template <typename F> std::future<json::object> Expect(F&& pred)
    {
        struct State {
            boost::signals2::connection _subscription;
            std::promise<json::object>  _promise;
        };

        auto state = std::make_shared<State>();

        state->_subscription = onMessage.connect( //
            [=, pred = std::forward<F>(pred)](json::object const& msg) {
                if (pred(msg)) {
                    state->_promise.set_value(msg);
                    state->_subscription.disconnect();
                }
            });

        return state->_promise.get_future();
    }

    static json::object msgId(json::object const& message) {
        return filtered(message, {"id", "sessionId"}); // TODO more ?
    };

    std::future<json::object> Request(json::object const& cmd)
    {
        auto fut = Expect([id = msgId(cmd)](json::object const& resp) {
            return msgId(resp) == id;
        });

        Send(cmd);

        return fut;
    }

    enum ActionResult { ContinueMonitoring, StopMonitoring };

    template <typename A, typename F>
    auto Monitor(A action, F&& filter = [](auto&&) noexcept { return true; })
    {
        struct State {
            boost::signals2::connection _subscription;
            std::promise<json::object>  _promise;
        };

        auto state = std::make_shared<State>();
        auto stop  = [state] { state->_subscription.disconnect(); };

        state->_subscription = onMessage.connect( //
            [=, filter = std::forward<F>(filter)](json::object const& msg) {
                if (filter(msg) && StopMonitoring == action(msg))
                    stop();
            });

        return stop; // gives the caller an "external" way to stop the monitor
    }

    void CloseConnection() {
        post(get_executor(), [this] {
            ws_.next_layer().cancel();
            ws_.async_close( //
                websocket::close_code::normal, [this](error_code ec) {
                    debug << "CloseConnection (" << ec.message() << ")" << std::endl;
                    onMessage.disconnect_all_slots();
                });
        });
    }

  private:
    // do_XXXX functions assumed to be on the strand
    beast::flat_buffer inbox_;

    void do_receive_loop() {
        debug << "do_receive_loop..." << std::endl;

        ws_.async_read(inbox_, [this](error_code ec, size_t n) {
            debug << "Received " << n << " bytes (" << ec.message() << ")" << std::endl;

            if (!ec) {
                auto text   = inbox_.cdata();
                auto parsed = json::parse(
                    {buffer_cast<char const*>(text), text.size()}, ec);
                inbox_.clear();

                if (!ec) {
                    assert(parsed.is_object());
                    onMessage(parsed.as_object()); // exceptions will blow up
                    do_receive_loop();
                } else {
                    debug << "Ignore failed parse (" << ec.message() << ")" << std::endl;
                }
            }
        });
    }

    std::deque<std::string> outbox_;

    void do_send_loop() {
        debug << "do_send_loop " << outbox_.size() << std::endl;
        if (outbox_.empty())
            return;

        ws_.async_write( //
            net::buffer(outbox_.front()), [this](error_code ec, size_t n) {
                debug << "Sent " << n << " bytes (" << ec.message() << ")" << std::endl;

                if (!ec) {
                    outbox_.pop_front();
                    do_send_loop();
                }
            });
    }
};

int main()
{
    net::thread_pool ioc(1);

    CDPClient client(ioc.get_executor());
    client.Connect("localhost", "9222", "/devtools/browser/bb8efece-b445-42d0-a4cc-349fccd8514d");

    auto trace = client.onMessage.connect([&](json::object const& obj) {
        std::cerr << " -- trace " << obj << std::endl;
    });

    unsigned id = 1; // TODO make per session

    auto targets = client.Request({
        {"id", id++},
        {"method", "Target.getTargets"},
    }).get().at("result").at("targetInfos");

    auto pageTarget = r::find_if(targets.as_array(), [](auto& info) {
        return info.at("type") == "page";
    });

    if (!pageTarget) {
        std::cerr << "No page target\n";
        return 0;
    }

    std::cout << "pageTarget " << *pageTarget << std::endl;
    auto sessionId = client.Request(
            {{"id", id++},
               {"method", "Target.attachToTarget"},
               {"params", json::object{
                   {"targetId", pageTarget->at("targetId")},
                   {"flatten", true},
               },
            }})
            .get().at("result").at("sessionId");

    std::cout << "sessionId: " << sessionId << std::endl;

    auto response = client.Request({
            {"sessionId", sessionId},
            {"id", 1}, // IDs are independent between sessions
            {"method", "Page.navigate"},
            {"params", json::object{
                 {"url", "https://stackoverflow.com/q/70768742/85371"},
             }},
        }) .get();

    std::cout << "Navigation response: " << response << std::endl;

    auto isMethod = [](auto value) {
        return [value](json::object const& msg) {
            auto m = msg.if_contains("method");
            return m && *m == value;
        };
    };

    auto loadingFinished = client.Expect(isMethod("Network.loadingFinished")).get();
    std::cout << "Got: " << loadingFinished << "\n";

    // monitor until 3 messages having an id divisable by 7 have been received
    std::atomic_int count = 0;

    auto stopMonitor = client.Monitor(
        [&count](json::object const& msg) {
            std::cout << "Divisable by 7: " << msg << "\n";
            return ++count >= 3 ? CDPClient::StopMonitoring
                                : CDPClient::ContinueMonitoring;
        },
        [](json::object const& msg) {
            auto id = msg.if_contains("id");
            return id && (0 == id->as_int64() % 7);
        });

    std::this_thread::sleep_for(5s);

    stopMonitor(); // even if 3 messages had not been reached, stop the monitor

    std::cout << count << " messages having an id divisable by 7 had been received in 5s\n";

    client.CloseConnection();

    ioc.join();
}

¹ besides some dinner

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Added a second version showing the same style as in Nodejs with promises. I think it is strictly more elegant (because less repeated/stateful code). – sehe Jan 19 '22 at 21:53
  • Amazing response... will need to test and digest everything. Second example is indeed way more elegant. Many thanks. – Mecanik Jan 19 '22 at 22:17
  • Sorry, I`m really lost with filtered() function. It seems it does not like the json object, won't compile no matter what I try :( – Mecanik Jan 20 '22 at 06:34
  • I managed to compile an test with the msgId() commented out, however I don't understand how you got those nice consistent results back from the server, because I don't get them properly. For example attachToTarget returns only 1 result, and later the second. Is it because I`m not using the msgId()? – Mecanik Jan 20 '22 at 09:29
  • Obviously. That [was the whole essence](https://chat.stackoverflow.com/transcript/message/53858336#53858336) all along. You can match the ID's in anywhich way. What compiler are you using? Obviously, the code compiles here. It's strange, why would `msgId()` not compile (it has nothing funky) and `#include ` is not a problem (that's c++20). I suspect that maybe you're using MSVC [:sad-trombone.wav:] – sehe Jan 20 '22 at 14:05
  • Normally I'd suggest https://godbolt.org/z/YEern561T or https://wandbox.org/permlink/HwbPvld6n1OuDJo7 but this code exceeds the resource limits... – sehe Jan 20 '22 at 14:07
  • Thank you! I used godbolt.org/z/YEern561T and it compiled (MSVC) and works fine so far! Hurray finally! – Mecanik Jan 20 '22 at 16:25
  • Oh, just realized I never included the full listing for the second version in the answer. Added. – sehe Jan 21 '22 at 02:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/241268/discussion-between-mecanik-and-sehe). – Mecanik Jan 21 '22 at 04:29
  • I would have one last question if you do not mind? Can I use somehow std::future::wait_until so I can find out if the page was loaded completely? (for example checking for Network.loadingFinished object)? – Mecanik Jan 21 '22 at 07:58
  • [Added](https://stackoverflow.com/posts/70777042/revisions) some proofs-of-concept in the new ["Comment After Care" answer section](https://stackoverflow.com/questions/70768742/chrome-devtools-protocol-using-boost-beast/70777042?noredirect=1#:~:text=Sure%2C%20just%20code%20it). – sehe Jan 21 '22 at 16:54
  • 1
    Wow... amazing. Thank you so much! I create my own little "waituntil" but cannot be compared with this. Also, I just realised that I need to extract a couple of values after "navigation" and your examples added now are PERFECT. – Mecanik Jan 21 '22 at 16:57
  • I just [fixed some formatting and simplified the switch](https://stackoverflow.com/posts/70777042/revisions) – sehe Jan 21 '22 at 17:04
  • Straight A... pretty cool stuff. Everything works perfectly, I`m just being a bit dumb on usage. I`m waiting with Expect for "Page.frameStoppedLoading"; all good. However, I need to extract "context id" for the loading page, otherwise I cannot continue doing anything. The message object is sent before "Page.frameStoppedLoading", looks like "{"method":"Runtime.executionContextCreated","params":{"context":{"id":5,"origin":"https://www.google.com",...}". Can you suggest me how to extract this from the huge reply sent back? – Mecanik Jan 21 '22 at 17:15
  • Also, just [refactored `Request` to be `Send` plus `Expect`](https://stackoverflow.com/posts/70777042/revisions), removing the duplication of code. Also rearranged the exposition of the answer to accomodate. Be sure to look at the refactored listing https://godbolt.org/z/eMoz9habc for reference. – sehe Jan 21 '22 at 17:23
  • The monitor function is one of the most amazing features you have added, it is extremely helpful. One of the most important things I want to do is intercept requests (Fetch.continueRequest/Fetch.failRequest). It "works", however it gets "stuck" when deploying the app. (weirdly, under debug mode in VS it works fine). From all the tests done the past days, I believe it is because I am sending data in the monitor, it causes UB. Even though I am using Send instead of Request (since I don't care about the result). Am I not supposed to send data in monitoring? Thanks. – Mecanik Jan 30 '22 at 09:55
  • Replied in chat – sehe Jan 30 '22 at 16:54
  • Dear sehe, should I open a new question about this? For the past weeks I have been intensively testing this inside different builds (console/GUI), unfortunately I am stumbled upon a problem that I cannot debug. The threads that run the code in your reply freeze randomly, forever. The main app runs normally. From playing around with the code I noticed it's from boost thread pool... but I`m unable to debug. – Mecanik Mar 10 '22 at 18:39
  • It means that completion handlers or take posted to the executor are blocking (like future::get). If you can show what causes the block I might recommend a way to fix that. – sehe Mar 10 '22 at 19:49
  • I see, I would like to show you... but I`m unable to. I debugged for hours and once it get's stuck, you cannot see where or why. I never seen anything like it... perhaps my debugging knowledge is not so extent. All I could notice is that if I change the thread pool, it gets stuck/frozen quicker. Chat? – Mecanik Mar 10 '22 at 20:07
  • The diagnosis is crystal clear. You're blocking the execution context threads. So, if you have the code, I can probably point at the offending line(s) in no time. – sehe Mar 10 '22 at 20:42
  • Chat room: https://chat.stackoverflow.com/rooms/info/242827/devtools-c-woes?tab=general – sehe Mar 10 '22 at 20:43