Revision control

++ // Copyright (C) 2024 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++ // clang-format off
++
++ //! [gen-includes]
++ #include "clientguide.qpb.h"
++ #include "clientguide_client.grpc.qpb.h"
++ //! [gen-includes]
++ #include "client_wrapper.h"
++ #include "interceptors.h"
++
++ #include <QtGrpc/QGrpcHttp2Channel>
++ #include <QtGrpc/qgrpcstream.h>
++
++ #include <QtCore/QCommandLineParser>
++ #include <QtCore/QCoreApplication>
++ #include <QtCore/QDateTime>
++ #include <QtCore/QDir>
++ #include <QtCore/QProcess>
++ #include <QtCore/QThread>
++ #include <QtCore/QTimer>
++ #include <QtCore/QUrl>
++
++ #include <limits>
++ #include <memory>
++
++ // We use part of the namespace to clarify the source.
++ using namespace client;
++ using namespace Qt::Literals::StringLiterals;
++
++ void startServerProcess();
++ QDebug operator<<(QDebug debug, const guide::Response &response);
++ enum class ExpectedResult { Success, Failure };
++
++ class ClientGuide : public QObject
++ {
++ public:
++ //! [basic-1]
++ explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
++ {
++ m_client.attachChannel(std::move(channel));
++ }
++ //! [basic-1]
++
++ //! [basic-2]
++ static guide::Request createRequest(int32_t num,
++ ExpectedResult expected = ExpectedResult::Success)
++ {
++ guide::Request request;
++ request.setNum(num);
++ // The server-side logic fails the RPC if the time is in the future.
++ const auto time = expected == ExpectedResult::Failure ?
++ std::numeric_limits<int64_t>::max() : now();
++ request.setTime(time);
++ return request;
++ }
++ //! [basic-2]
++
++ //! [unary-0]
++ void unaryCall(const guide::Request &request, const QGrpcCallOptions &opts = { })
++ {
++ std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request, opts);
++ const auto *replyPtr = reply.get();
++ connect(
++ replyPtr, &QGrpcCallReply::finished, replyPtr,
++ [reply = std::move(reply)](const QGrpcStatus &status) {
++ if (status.isOk()) {
++ if (const auto response = reply->read<guide::Response>())
++ qDebug() << "Client (UnaryCall) finished, received:" << *response;
++ else
++ qDebug("Client (UnaryCall) deserialization failed");
++ } else {
++ qDebug() << "Client (UnaryCall) failed:" << status;
++ }
++ },
++ Qt::SingleShotConnection);
++ }
++ //! [unary-0]
++
++ //! [sstream-0]
++ void serverStreaming(const guide::Request &initialRequest)
++ {
++ std::unique_ptr<QGrpcServerStream> stream = m_client.ServerStreaming(initialRequest);
++ const auto *streamPtr = stream.get();
++
++ connect(
++ streamPtr, &QGrpcServerStream::finished, streamPtr,
++ [stream = std::move(stream)](const QGrpcStatus &status) {
++ if (status.isOk())
++ qDebug("Client (ServerStreaming) finished");
++ else
++ qDebug() << "Client (ServerStreaming) failed:" << status;
++ },
++ Qt::SingleShotConnection);
++ //! [sstream-0]
++ //! [sstream-1]
++ connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] {
++ if (const auto response = streamPtr->read<guide::Response>())
++ qDebug() << "Client (ServerStream) received:" << *response;
++ else
++ qDebug("Client (ServerStream) deserialization failed");
++ });
++ }
++ //! [sstream-1]
++
++ // ! [cstream-0]
++ void clientStreaming(const guide::Request &initialRequest)
++ {
++ m_clientStream = m_client.ClientStreaming(initialRequest);
++
++ // Pace the writes: send the next message only once messageWritten reports
++ // the previous one accepted by the channel. This respects HTTP/2 flow
++ // control and keeps the outgoing queue bounded, unlike writing every
++ // message up front.
++ connect(m_clientStream.get(), &QGrpcClientStream::messageWritten, m_clientStream.get(),
++ [this, initialRequest, sent = 1]() mutable {
++ if (sent < 3)
++ m_clientStream->writeMessage(createRequest(initialRequest.num() + sent++));
++ else
++ m_clientStream->writesDone();
++ });
++
++ connect(m_clientStream.get(), &QGrpcClientStream::finished, m_clientStream.get(),
++ [this](const QGrpcStatus &status) {
++ if (status.isOk()) {
++ if (const auto response = m_clientStream->read<guide::Response>()) {
++ qDebug() << "Client (ClientStreaming) finished, received:"
++ << *response;
++ }
++ m_clientStream.reset();
++ } else {
++ qDebug() << "Client (ClientStreaming) failed:" << status;
++ qDebug("Restarting the client stream");
++ clientStreaming(createRequest(0));
++ }
++ });
++ }
++ // ! [cstream-0]
++
++ // ! [bstream-1]
++ void bidirectionalStreaming(const guide::Request &initialRequest)
++ {
++ m_bidiStream = m_client.BidirectionalStreaming(initialRequest);
++ connect(m_bidiStream.get(), &QGrpcBidiStream::finished, this, &ClientGuide::bidiFinished);
++ connect(m_bidiStream.get(), &QGrpcBidiStream::messageReceived, this,
++ &ClientGuide::bidiMessageReceived);
++ }
++ // ! [bstream-1]
++
++ private slots:
++ // ! [bstream-2]
++ void bidiFinished(const QGrpcStatus &status)
++ {
++ if (status.isOk())
++ qDebug("Client (BidirectionalStreaming) finished");
++ else
++ qDebug() << "Client (BidirectionalStreaming) failed:" << status;
++ m_bidiStream.reset();
++ }
++ // ! [bstream-2]
++
++ // ! [bstream-3]
++ void bidiMessageReceived()
++ {
++ if (m_bidiStream->read(&m_bidiResponse)) {
++ qDebug() << "Client (BidirectionalStreaming) received:" << m_bidiResponse;
++ if (m_bidiResponse.num() > 0) {
++ m_bidiStream->writeMessage(createRequest(m_bidiResponse.num() - 1));
++ return;
++ }
++ } else {
++ qDebug("Client (BidirectionalStreaming) deserialization failed");
++ }
++ m_bidiStream->writesDone();
++ }
++ // ! [bstream-3]
++
++ private:
++ ClientGuideServiceWrapper m_client;
++ std::unique_ptr<QGrpcClientStream> m_clientStream;
++ // ! [bstream-0]
++ std::unique_ptr<QGrpcBidiStream> m_bidiStream;
++ guide::Response m_bidiResponse;
++ // ! [bstream-0]
++ };
++
++ int main(int argc, char *argv[])
++ {
++ QCoreApplication app(argc, argv);
++
++ // Use the -U, -S, -C, -B options to control execution
++ QCommandLineParser parser;
++ QCommandLineOption enableUnary("U", "Enable UnaryCalls");
++ QCommandLineOption enableSStream("S", "Enable ServerStream");
++ QCommandLineOption enableCStream("C", "Enable ClientStream");
++ QCommandLineOption enableBStream("B", "Enable BiDiStream");
++ // Use -I to enable client-side interceptor. Works in combination with -U and -B.
++ QCommandLineOption enableInterceptors("I", "Enable Interceptors");
++
++ parser.addHelpOption();
++ parser.addOption(enableUnary);
++ parser.addOption(enableSStream);
++ parser.addOption(enableCStream);
++ parser.addOption(enableBStream);
++ parser.addOption(enableInterceptors);
++ parser.process(app);
++
++ bool defaultRun = !parser.isSet(enableUnary) && !parser.isSet(enableSStream)
++ && !parser.isSet(enableCStream) && !parser.isSet(enableBStream);
++
++ qDebug("Welcome to the clientguide!");
++ if (parser.isSet(enableInterceptors))
++ qDebug("Running with Interceptor support");
++ qDebug("Starting the server process ...");
++ startServerProcess();
++
++ std::shared_ptr<QGrpcHttp2Channel> channel;
++ if (parser.isSet(enableInterceptors)) {
++ //! [interceptorchain-1]
++ auto interceptors = createInterceptors();
++ if (interceptors.isEmpty()) {
++ qWarning("Failed to create the interceptor chain");
++ return EXIT_FAILURE; // or some other suitable fallback
++ }
++ channel = std::make_shared<QGrpcHttp2Channel>(
++ std::move(interceptors)
++ );
++ //! [interceptorchain-1]
++ } else {
++ //! [basic-0a]
++ channel = std::make_shared<QGrpcHttp2Channel>(
++ /* without channel options. */
++ );
++ //! [basic-0a]
++ }
++
++ //! [basic-0b]
++ ClientGuide clientGuide(channel);
++ //! [basic-0b]
++
++ if (parser.isSet(enableInterceptors)) {
++ //! [interceptors-0a]
++ int delayMs = 150;
++ //! [interceptors-0a]
++ if (defaultRun || parser.isSet(enableUnary)) {
++ //! [interceptors-0b]
++ for (int i = 1; i < 6; ++i) {
++ QTimer::singleShot(delayMs, [&, i] {
++ auto expected = i % 2 == 0 ? ExpectedResult::Failure : ExpectedResult::Success;
++ clientGuide.unaryCall(ClientGuide::createRequest(i, expected));
++ });
++ delayMs += 150;
++ }
++ QTimer::singleShot(delayMs, [&] {
++ QGrpcCallOptions invalidOpts;
++ invalidOpts.addMetadata("huge_key"_ba, "huge_value"_ba.repeated(8'000));
++ clientGuide.unaryCall(guide::Request{ }, invalidOpts); // this call will fail.
++ });
++ delayMs += 150;
++ //! [interceptors-0b]
++ }
++
++ if (defaultRun || parser.isSet(enableBStream)) {
++ //! [interceptors-1]
++ QTimer::singleShot(delayMs, [&] {
++ clientGuide.bidirectionalStreaming(ClientGuide::createRequest(666));
++ });
++ //! [interceptors-1]
++ }
++ } else {
++ if (defaultRun || parser.isSet(enableUnary)) {
++ //! [unary-1]
++ clientGuide.unaryCall(ClientGuide::createRequest(1));
++ clientGuide.unaryCall(ClientGuide::createRequest(2, ExpectedResult::Failure));
++ clientGuide.unaryCall(ClientGuide::createRequest(3));
++ //! [unary-1]
++ }
++
++ if (defaultRun || parser.isSet(enableSStream)) {
++ //! [sstream-2]
++ clientGuide.serverStreaming(ClientGuide::createRequest(3));
++ // ! [sstream-2]
++ }
++
++ if (defaultRun || parser.isSet(enableCStream)) {
++ // ! [cstream-1]
++ clientGuide.clientStreaming(ClientGuide::createRequest(0, ExpectedResult::Failure));
++ // ! [cstream-1]
++ }
++
++ if (defaultRun || parser.isSet(enableBStream)) {
++ // ! [bstream-4]
++ clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));
++ // ! [bstream-4]
++ }
++ }
++
++ return QCoreApplication::exec();
++ }
++
++ void startServerProcess()
++ {
++ // For the purpose of this example, we launch the server directly from the
++ // client. In a real-world scenario, the server should be running
++ // independently, and this code would not be necessary. This approach is
++ // used here solely for convenience in demonstrating the full interaction.
++ static QProcess serverProcess;
++ QObject::connect(&serverProcess, &QProcess::readyReadStandardOutput, [] {
++ while (serverProcess.canReadLine()) {
++ QByteArray line = serverProcess.readLine().trimmed();
++ if (!line.isEmpty())
++ qDebug().noquote().nospace() << " " << line;
++ }
++ });
++ serverProcess.setProcessChannelMode(QProcess::MergedChannels);
++ serverProcess.setReadChannel(QProcess::StandardOutput);
++ // The server must be located next to the client.
++ serverProcess.start(QCoreApplication::applicationDirPath()
++ + QDir::separator() + SERVER_FILE_NAME);
++ if (!serverProcess.waitForStarted()) {
++ qFatal() << "Couldn't start the server: " << serverProcess.errorString();
++ exit(EXIT_FAILURE);
++ }
++ QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, [] {
++ if (serverProcess.state() == QProcess::Running) {
++ serverProcess.kill();
++ serverProcess.waitForFinished(5'000); // 5s timeout
++ }
++ });
++ // give the process some time to properly start up the server
++ QThread::sleep(1);
++ }
++
++ QDebug operator<<(QDebug debug, const guide::Response &response)
++ {
++ QDebugStateSaver saver(debug);
++ return debug.nospace() << "Response( time: " << response.time()
++ << ", num: " << response.num() << " )";
++ }