diff options
author | Marius Vlad <marius.vlad@collabora.com> | 2022-10-25 13:06:41 +0300 |
---|---|---|
committer | Marius Vlad <marius.vlad@collabora.com> | 2022-12-13 12:31:18 +0200 |
commit | 59375972f5642b7ec5115fdecf4828f6af02f343 (patch) | |
tree | 14912e10b5894e0d3eef773ed3306f455352b9ec | |
parent | 0b766cf978b8b100caecd4c61464e1a683685072 (diff) |
grpc-proxy: Init gRPCsandbox/mvlad/grpc-async-cb
This brings in support for accessing agl-shell protocol indirectly by
using a gRPC interface which bridges the communication between a
particular client (the client issuing gRPC requests) and the AGL
compositor which does that by re-using the same agl-shell protocol.
In order to achieve that, and further more, to avoid having ifdefs code
in the compositor and deal with threading, we instead resorted to using
a helper client.
On one side this helper implements the gRPC server API,
and on the other, a wayland native client that implements the
agl_shell interface.
It uses the agl_shell_ext interface added
previously to communicate with the compositor that it requires access
to agl_shell interface as well. The helper expects that agl_shell interface
was already bounded to another client before starting it so it waits
until that happens and then it implements the protocol specification,
for each interface.
Launching the helper client automatically can be done by adding the
following entry to the ini file:
[shell-client-ext]
command=/path/to/agl-shell-grpc-server
The gRPC server implementation only handles the agl_shell interface
until to this point, specifically, the activate_app request, and the
events that were adedd with version 3 of the agl-shell protocol.
Also the implementation uses the Reactor pattern, with Callback service
that greatly simplifies the async version and avoids putting locks to
to handle multiple clients. This should allow multiple clients being
connected to the gRPC server and receive events / send requests.
Bug-AGL: SPEC-4503
Signed-off-by: Marius Vlad <marius.vlad@collabora.com>
Change-Id: Ie870da3caa138394d8dd30f9d22a5552d585d63a
-rw-r--r-- | grpc-proxy/agl_shell.proto | 62 | ||||
-rw-r--r-- | grpc-proxy/grpc-async-cb.cpp | 166 | ||||
-rw-r--r-- | grpc-proxy/grpc-async-cb.h | 93 | ||||
-rw-r--r-- | grpc-proxy/log.h | 11 | ||||
-rw-r--r-- | grpc-proxy/main-grpc.cpp | 568 | ||||
-rw-r--r-- | grpc-proxy/main-grpc.h | 64 | ||||
-rw-r--r-- | grpc-proxy/meson.build | 46 | ||||
-rw-r--r-- | grpc-proxy/shell.cpp | 78 | ||||
-rw-r--r-- | grpc-proxy/shell.h | 45 | ||||
-rw-r--r-- | meson.build | 4 | ||||
-rw-r--r-- | meson_options.txt | 7 |
11 files changed, 1144 insertions, 0 deletions
diff --git a/grpc-proxy/agl_shell.proto b/grpc-proxy/agl_shell.proto new file mode 100644 index 0000000..22fad1c --- /dev/null +++ b/grpc-proxy/agl_shell.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; +// using empty Response suitable better for forward compat +//import "google/protobuf/empty.proto"; +package agl_shell_ipc; + +service AglShellManagerService { + rpc ActivateApp(ActivateRequest) returns (ActivateResponse) {} + rpc DeactivateApp(DeactivateRequest) returns (DeactivateResponse) {} + rpc SetAppSplit(SplitRequest) returns (SplitResponse) {} + rpc SetAppFloat(FloatRequest) returns (FloatResponse) {} + rpc AppStatusState(AppStateRequest) returns (stream AppStateResponse) {} + rpc GetOutputs(OutputRequest) returns (ListOutputResponse) {} +} + +message ActivateRequest { + string app_id = 1; + string output_name = 2; +} + +message ActivateResponse { +}; + +message DeactivateRequest { + string app_id = 1; +} + +message DeactivateResponse { +} + +message SplitRequest { + string app_id = 1; + int32 tile_orientation = 2; +} + +message SplitResponse { +} + +message FloatRequest { + string app_id = 1; +} + +message FloatResponse { +} + +message AppStateRequest { +} + +message AppStateResponse { + int32 state = 1; + string app_id = 2; +} + +message OutputRequest { +}; + +message OutputResponse { + string name = 1; +}; + +message ListOutputResponse { + repeated OutputResponse outputs = 1; +}; diff --git a/grpc-proxy/grpc-async-cb.cpp b/grpc-proxy/grpc-async-cb.cpp new file mode 100644 index 0000000..d0fd88d --- /dev/null +++ b/grpc-proxy/grpc-async-cb.cpp @@ -0,0 +1,166 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <cstdio> +#include <ctime> +#include <algorithm> +#include <queue> + +#define GRPC_CALLBACK_API_NONEXPERIMENTAL + +#include <grpc/grpc.h> +#include <grpcpp/grpcpp.h> +#include <grpcpp/server.h> +#include <grpcpp/server_builder.h> +#include <grpcpp/server_context.h> + +#include <grpcpp/ext/proto_server_reflection_plugin.h> +#include <grpcpp/health_check_service_interface.h> + +#include "log.h" +#include "agl_shell.grpc.pb.h" +#include "grpc-async-cb.h" + +Lister::Lister(Shell *shell) : m_shell(shell) +{ + // don't call NextWrite() just yet we do it explicitly when getting + // the events from the compositor + m_writting = false; +} + +void +Lister::OnDone() +{ + delete this; +} + +void Lister::OnWriteDone(bool ok) +{ + LOG("got ok %d\n", ok); + if (ok) { + LOG("done writting %d\n", m_writting); + m_writting = false; + } +} + +void +Lister::NextWrite(void) +{ + if (m_writting) { + LOG(">>>>> still in writting\n"); + return; + } + m_writting = true; + StartWrite(&m_shell->m_shell_data->current_app_state); +} + +bool +Lister::Writting(void) +{ + return m_writting; +} + +grpc::ServerUnaryReactor * +GrpcServiceImpl::ActivateApp(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::ActivateRequest* request, + ::agl_shell_ipc::ActivateResponse* /*response*/) +{ + LOG("activating app %s on output %s\n", request->app_id().c_str(), + request->output_name().c_str()); + + m_aglShell->ActivateApp(request->app_id(), request->output_name()); + + grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); + reactor->Finish(grpc::Status::OK); + return reactor; +} + +grpc::ServerUnaryReactor * +GrpcServiceImpl::DeactivateApp(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::DeactivateRequest* request, + ::agl_shell_ipc::DeactivateResponse* /*response*/) +{ + m_aglShell->DeactivateApp(request->app_id()); + + grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); + reactor->Finish(grpc::Status::OK); + return reactor; +} + +grpc::ServerUnaryReactor * +GrpcServiceImpl::SetAppFloat(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::FloatRequest* request, + ::agl_shell_ipc::FloatResponse* /* response */) +{ + m_aglShell->SetAppFloat(request->app_id()); + + grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); + reactor->Finish(grpc::Status::OK); + return reactor; +} + +grpc::ServerUnaryReactor * +GrpcServiceImpl::SetAppSplit(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::SplitRequest* request, + ::agl_shell_ipc::SplitResponse* /*response*/) +{ + m_aglShell->SetAppSplit(request->app_id(), request->tile_orientation()); + + grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); + reactor->Finish(grpc::Status::OK); + return reactor; +} + +grpc::ServerUnaryReactor * +GrpcServiceImpl::GetOutputs(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::OutputRequest* /* request */, + ::agl_shell_ipc::ListOutputResponse* response) +{ + struct window_output *output; + + struct wl_list *list = &m_aglShell->m_shell_data->output_list; + wl_list_for_each(output, list, link) { + auto m_output = response->add_outputs(); + m_output->set_name(output->name); + } + + grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); + reactor->Finish(grpc::Status::OK); + return reactor; +} + +grpc::ServerWriteReactor<::agl_shell_ipc::AppStateResponse>* +GrpcServiceImpl::AppStatusState(grpc::CallbackServerContext* context, + const ::agl_shell_ipc::AppStateRequest* /*request */) +{ + + Lister *n = new Lister(m_aglShell); + + m_aglShell->m_shell_data->server_context_list.push_back(std::pair(context, n)); + LOG("added lister %p\n", static_cast<void *>(n)); + + // just return a Lister to keep the channel open + return n; +} diff --git a/grpc-proxy/grpc-async-cb.h b/grpc-proxy/grpc-async-cb.h new file mode 100644 index 0000000..e6a19a6 --- /dev/null +++ b/grpc-proxy/grpc-async-cb.h @@ -0,0 +1,93 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include <memory> + +#define GRPC_CALLBACK_API_NONEXPERIMENTAL + +#include <grpc/grpc.h> +#include <grpcpp/grpcpp.h> +#include <grpcpp/server.h> +#include <grpcpp/server_builder.h> +#include <grpcpp/server_context.h> + +#include <mutex> +#include <condition_variable> + +#include <grpcpp/ext/proto_server_reflection_plugin.h> +#include <grpcpp/health_check_service_interface.h> + +#include "shell.h" +#include "agl_shell.grpc.pb.h" + +namespace { + const char kDefaultGrpcServiceAddress[] = "127.0.0.1:14005"; +} + +class Lister : public grpc::ServerWriteReactor<::agl_shell_ipc::AppStateResponse> { +public: + Lister(Shell *aglShell); + void OnDone() override; + void OnWriteDone(bool ok) override; + void NextWrite(void); + bool Writting(void); +private: + Shell *m_shell; + bool m_writting; +}; + +class GrpcServiceImpl final : public agl_shell_ipc::AglShellManagerService::CallbackService { +public: + GrpcServiceImpl(Shell *aglShell) : m_aglShell(aglShell) {} + + grpc::ServerUnaryReactor *ActivateApp(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::ActivateRequest* request, + ::agl_shell_ipc::ActivateResponse* /*response*/) override; + + grpc::ServerUnaryReactor *DeactivateApp(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::DeactivateRequest* request, + ::agl_shell_ipc::DeactivateResponse* /*response*/) override; + + grpc::ServerUnaryReactor *SetAppFloat(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::FloatRequest* request, + ::agl_shell_ipc::FloatResponse* /*response*/) override; + + grpc::ServerUnaryReactor *SetAppSplit(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::SplitRequest* request, + ::agl_shell_ipc::SplitResponse* /*response*/) override; + + grpc::ServerUnaryReactor *GetOutputs(grpc::CallbackServerContext *context, + const ::agl_shell_ipc::OutputRequest* /* request */, + ::agl_shell_ipc::ListOutputResponse* response) override; + + grpc::ServerWriteReactor< ::agl_shell_ipc::AppStateResponse>* AppStatusState( + ::grpc::CallbackServerContext* /*context*/, + const ::agl_shell_ipc::AppStateRequest* /*request*/) override; + +private: + Shell *m_aglShell; +}; diff --git a/grpc-proxy/log.h b/grpc-proxy/log.h new file mode 100644 index 0000000..d0a5275 --- /dev/null +++ b/grpc-proxy/log.h @@ -0,0 +1,11 @@ +#pragma once + +#include <cstdio> + +//#define DEBUG + +#if !defined(LOG) && defined(DEBUG) +#define LOG(fmt, ...) do { fprintf(stderr, "%s() " fmt, __func__, ##__VA_ARGS__); } while (0) +#else +#define LOG(fmt, ...) do {} while (0) +#endif diff --git a/grpc-proxy/main-grpc.cpp b/grpc-proxy/main-grpc.cpp new file mode 100644 index 0000000..a59c282 --- /dev/null +++ b/grpc-proxy/main-grpc.cpp @@ -0,0 +1,568 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <cstdio> +#include <ctime> +#include <algorithm> +#include <queue> +#include <thread> +#include <mutex> +#include <condition_variable> + +#include "shell.h" +#include "log.h" +#include "main-grpc.h" +#include "grpc-async-cb.h" + +struct shell_data_init { + struct agl_shell *shell; + bool wait_for_bound; + bool bound_ok; + bool bound_fail; + int version; +}; + +static int running = 1; + +static void +agl_shell_bound_ok_init(void *data, struct agl_shell *agl_shell) +{ + (void) agl_shell; + + struct shell_data_init *sh = static_cast<struct shell_data_init *>(data); + sh->wait_for_bound = false; + + sh->bound_ok = true; +} + +static void +agl_shell_bound_fail_init(void *data, struct agl_shell *agl_shell) +{ + (void) agl_shell; + + struct shell_data_init *sh = static_cast<struct shell_data_init *>(data); + sh->wait_for_bound = false; + + sh->bound_fail = true; +} + +static void +agl_shell_bound_ok(void *data, struct agl_shell *agl_shell) +{ + (void) agl_shell; + + struct shell_data *sh = static_cast<struct shell_data *>(data); + sh->wait_for_bound = false; + + sh->bound_ok = true; +} + +static void +agl_shell_bound_fail(void *data, struct agl_shell *agl_shell) +{ + (void) agl_shell; + + struct shell_data *sh = static_cast<struct shell_data *>(data); + sh->wait_for_bound = false; + + sh->bound_ok = false; +} + +static void +agl_shell_app_state(void *data, struct agl_shell *agl_shell, + const char *app_id, uint32_t state) +{ + (void) agl_shell; + struct shell_data *sh = static_cast<struct shell_data *>(data); + LOG("got app_state event app_id %s, state %d\n", app_id, state); + + if (sh->server_context_list.empty()) + return; + + ::agl_shell_ipc::AppStateResponse app; + + sh->current_app_state.set_app_id(std::string(app_id)); + sh->current_app_state.set_state(state); + + auto start = sh->server_context_list.begin(); + while (start != sh->server_context_list.end()) { + // hold on if we're still detecting another in-flight writting + if (start->second->Writting()) { + LOG("skip writing to lister %p\n", static_cast<void *>(start->second)); + continue; + } + + LOG("writing to lister %p\n", static_cast<void *>(start->second)); + start->second->NextWrite(); + start++; + } +} + +static const struct agl_shell_listener shell_listener = { + agl_shell_bound_ok, + agl_shell_bound_fail, + agl_shell_app_state, +}; + +static const struct agl_shell_listener shell_listener_init = { + agl_shell_bound_ok_init, + agl_shell_bound_fail_init, + nullptr, +}; + +static void +agl_shell_ext_doas_done(void *data, struct agl_shell_ext *agl_shell_ext, uint32_t status) +{ + (void) agl_shell_ext; + + struct shell_data *sh = static_cast<struct shell_data *>(data); + sh->wait_for_doas = false; + + if (status == AGL_SHELL_EXT_DOAS_SHELL_CLIENT_STATUS_SUCCESS) + sh->doas_ok = true; +} + +static const struct agl_shell_ext_listener shell_ext_listener = { + agl_shell_ext_doas_done, +}; + +static void +display_handle_geometry(void *data, struct wl_output *wl_output, + int x, int y, int physical_width, int physical_height, + int subpixel, const char *make, const char *model, int transform) +{ + (void) data; + (void) wl_output; + (void) x; + (void) y; + (void) physical_width; + (void) physical_height; + (void) subpixel; + (void) make; + (void) model; + (void) transform; +} + +static void +display_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int width, int height, int refresh) +{ + (void) data; + (void) wl_output; + (void) flags; + (void) width; + (void) height; + (void) refresh; +} + +static void +display_handle_done(void *data, struct wl_output *wl_output) +{ + (void) data; + (void) wl_output; +} + +static void +display_handle_scale(void *data, struct wl_output *wl_output, int32_t factor) +{ + (void) data; + (void) wl_output; + (void) factor; +} + + +static void +display_handle_name(void *data, struct wl_output *wl_output, const char *name) +{ + (void) wl_output; + + struct window_output *woutput = static_cast<struct window_output *>(data); + woutput->name = strdup(name); +} + +static void +display_handle_description(void *data, struct wl_output *wl_output, const char *description) +{ + (void) data; + (void) wl_output; + (void) description; +} + +static const struct wl_output_listener output_listener = { + display_handle_geometry, + display_handle_mode, + display_handle_done, + display_handle_scale, + display_handle_name, + display_handle_description, +}; + +static void +display_add_output(struct shell_data *sh, struct wl_registry *reg, + uint32_t id, uint32_t version) +{ + struct window_output *w_output; + + w_output = new struct window_output; + w_output->shell_data = sh; + + w_output->output = + static_cast<struct wl_output *>(wl_registry_bind(reg, id, + &wl_output_interface, + std::min(version, static_cast<uint32_t>(4)))); + + wl_list_insert(&sh->output_list, &w_output->link); + wl_output_add_listener(w_output->output, &output_listener, w_output); +} + +static void +destroy_output(struct window_output *w_output) +{ + free(w_output->name); + wl_list_remove(&w_output->link); + free(w_output); +} + +static void +global_add(void *data, struct wl_registry *reg, uint32_t id, + const char *interface, uint32_t version) +{ + + struct shell_data *sh = static_cast<struct shell_data *>(data); + + if (!sh) + return; + + if (strcmp(interface, agl_shell_interface.name) == 0) { + // bind to at least v3 to get events + sh->shell = + static_cast<struct agl_shell *>(wl_registry_bind(reg, id, + &agl_shell_interface, + std::min(static_cast<uint32_t>(3), version))); + agl_shell_add_listener(sh->shell, &shell_listener, data); + sh->version = version; + } else if (strcmp(interface, "wl_output") == 0) { + display_add_output(sh, reg, id, version); + } +} + +// the purpose of this _init is to make sure we're not the first shell client +// running to allow the 'main' shell client take over. +static void +global_add_init(void *data, struct wl_registry *reg, uint32_t id, + const char *interface, uint32_t version) +{ + + struct shell_data_init *sh = static_cast<struct shell_data_init *>(data); + + if (!sh) + return; + + if (strcmp(interface, agl_shell_interface.name) == 0) { + sh->shell = + static_cast<struct agl_shell *>(wl_registry_bind(reg, id, + &agl_shell_interface, + std::min(static_cast<uint32_t>(3), version))); + agl_shell_add_listener(sh->shell, &shell_listener_init, data); + sh->version = version; + } +} + +static void +global_remove(void *data, struct wl_registry *reg, uint32_t id) +{ + /* Don't care */ + (void) data; + (void) reg; + (void) id; +} + +static void +global_add_ext(void *data, struct wl_registry *reg, uint32_t id, + const char *interface, uint32_t version) +{ + struct shell_data *sh = static_cast<struct shell_data *>(data); + + if (!sh) + return; + + if (strcmp(interface, agl_shell_ext_interface.name) == 0) { + sh->shell_ext = + static_cast<struct agl_shell_ext *>(wl_registry_bind(reg, id, + &agl_shell_ext_interface, + std::min(static_cast<uint32_t>(1), version))); + agl_shell_ext_add_listener(sh->shell_ext, + &shell_ext_listener, data); + } +} + +static const struct wl_registry_listener registry_ext_listener = { + global_add_ext, + global_remove, +}; + +static const struct wl_registry_listener registry_listener = { + global_add, + global_remove, +}; + +static const struct wl_registry_listener registry_listener_init = { + global_add_init, + global_remove, +}; + +static void +register_shell_ext(struct wl_display *wl_display, struct shell_data *sh) +{ + struct wl_registry *registry; + + registry = wl_display_get_registry(wl_display); + + wl_registry_add_listener(registry, ®istry_ext_listener, sh); + + wl_display_roundtrip(wl_display); + wl_registry_destroy(registry); +} + +static void +register_shell(struct wl_display *wl_display, struct shell_data *sh) +{ + struct wl_registry *registry; + + wl_list_init(&sh->output_list); + + registry = wl_display_get_registry(wl_display); + + wl_registry_add_listener(registry, ®istry_listener, sh); + + wl_display_roundtrip(wl_display); + wl_registry_destroy(registry); +} + +static int +__register_shell_init(void) +{ + int ret = 0; + struct wl_registry *registry; + struct wl_display *wl_display; + + struct shell_data_init *sh = new struct shell_data_init; + + wl_display = wl_display_connect(NULL); + registry = wl_display_get_registry(wl_display); + sh->wait_for_bound = true; + sh->bound_fail = false; + sh->bound_ok = false; + + wl_registry_add_listener(registry, ®istry_listener_init, sh); + wl_display_roundtrip(wl_display); + + if (!sh->shell || sh->version < 3) { + ret = -1; + goto err; + } + + while (ret !=- 1 && sh->wait_for_bound) { + ret = wl_display_dispatch(wl_display); + + if (sh->wait_for_bound) + continue; + } + + ret = sh->bound_fail; + + agl_shell_destroy(sh->shell); + wl_display_flush(wl_display); +err: + wl_registry_destroy(registry); + wl_display_disconnect(wl_display); + delete sh; + return ret; +} + +// we expect this client to be up & running *after* the shell client has +// already set-up panels/backgrounds. +// this means the very first try to bind to agl_shell we wait for +// 'bound_fail' event, which would tell us when it's ok to attempt to +// bind agl_shell_ext, call doas request, then attempt to bind (one +// more time) to agl_shell but this time wait for 'bound_ok' event. +void +register_shell_init(void) +{ + struct timespec ts = {}; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + ts.tv_sec = 0; + ts.tv_nsec = 250 * 1000 * 1000; // 250 ms + + // verify if 'bound_fail' was received + while (true) { + + int r = __register_shell_init(); + + if (r < 0) { + LOG("agl-shell extension not found or version too low\n"); + exit(EXIT_FAILURE); + } else if (r == 1) { + // we need to get a 'bound_fail' event, if we get a 'bound_ok' + // it means we're the first shell to start so wait until the + // shell client actually started + LOG("Found another shell client running. " + "Going further to bind to the agl_shell_ext interface\n"); + break; + } + + LOG("No shell client detected running. Will wait until one starts up...\n"); + nanosleep(&ts, NULL); + } + +} + +static void +destroy_shell_data(struct shell_data *sh) +{ + struct window_output *w_output, *w_output_next; + + wl_list_for_each_safe(w_output, w_output_next, &sh->output_list, link) + destroy_output(w_output); + + wl_display_flush(sh->wl_display); + wl_display_disconnect(sh->wl_display); + + delete sh; +} + +static struct shell_data * +start_agl_shell_client(void) +{ + int ret = 0; + struct wl_display *wl_display; + + wl_display = wl_display_connect(NULL); + + struct shell_data *sh = new struct shell_data; + + sh->wl_display = wl_display; + sh->wait_for_doas = true; + sh->wait_for_bound = true; + + register_shell_ext(wl_display, sh); + + // check for agl_shell_ext + if (!sh->shell_ext) { + LOG("Failed to bind to agl_shell_ext interface\n"); + goto err; + } + + if (wl_list_empty(&sh->output_list)) { + LOG("Failed get any outputs!\n"); + goto err; + } + + agl_shell_ext_doas_shell_client(sh->shell_ext); + while (ret != -1 && sh->wait_for_doas) { + ret = wl_display_dispatch(sh->wl_display); + if (sh->wait_for_doas) + continue; + } + + if (!sh->doas_ok) { + LOG("Failed to get doas_done event\n"); + goto err; + } + + // bind to agl-shell + register_shell(wl_display, sh); + while (ret != -1 && sh->wait_for_bound) { + ret = wl_display_dispatch(sh->wl_display); + if (sh->wait_for_bound) + continue; + } + + // at this point, we can't do anything about it + if (!sh->bound_ok) { + LOG("Failed to get bound_ok event!\n"); + goto err; + } + + LOG("agl_shell/agl_shell_ext interface OK\n"); + + return sh; +err: + delete sh; + return nullptr; +} + +static void +start_grpc_server(Shell *aglShell) +{ + // instantiante the grpc server + std::string server_address(kDefaultGrpcServiceAddress); + GrpcServiceImpl service{aglShell}; + + grpc::EnableDefaultHealthCheckService(true); + grpc::reflection::InitProtoReflectionServerBuilderPlugin(); + + grpc::ServerBuilder builder; + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + builder.RegisterService(&service); + + std::unique_ptr<grpc::Server> server(builder.BuildAndStart()); + LOG("gRPC server listening on %s\n", server_address.c_str()); + + server->Wait(); +} + +int main(int argc, char **argv) +{ + (void) argc; + (void) argv; + Shell *aglShell; + int ret = 0; + + // this blocks until we detect that another shell client started + // running + register_shell_init(); + + struct shell_data *sh = start_agl_shell_client(); + if (!sh) { + LOG("Failed to initialize agl-shell/agl-shell-ext\n"); + exit(EXIT_FAILURE); + } + + std::shared_ptr<struct agl_shell> agl_shell{sh->shell, agl_shell_destroy}; + aglShell = new Shell(agl_shell, sh); + + std::thread thread(start_grpc_server, aglShell); + + // serve wayland requests + while (running && ret != -1) { + ret = wl_display_dispatch(sh->wl_display); + } + + destroy_shell_data(sh); + return 0; +} diff --git a/grpc-proxy/main-grpc.h b/grpc-proxy/main-grpc.h new file mode 100644 index 0000000..282600d --- /dev/null +++ b/grpc-proxy/main-grpc.h @@ -0,0 +1,64 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include <cstdio> +#include <algorithm> +#include <queue> +#include <mutex> +#include <condition_variable> +#include <wayland-client.h> + +#define GRPC_CALLBACK_API_NONEXPERIMENTAL + +#include "agl_shell.grpc.pb.h" + +// forward declaration created in grpc-async-cb +class Lister; + +struct shell_data { + struct wl_display *wl_display; + struct agl_shell *shell; + struct agl_shell_ext *shell_ext; + + bool wait_for_bound; + bool wait_for_doas; + + bool bound_ok; + bool doas_ok; + + uint32_t version; + struct wl_list output_list; /** window_output::link */ + + ::agl_shell_ipc::AppStateResponse current_app_state; + std::list<std::pair<grpc::CallbackServerContext*, Lister *> > server_context_list; +}; + +struct window_output { + struct shell_data *shell_data; + struct wl_output *output; + char *name; + struct wl_list link; /** display::output_list */ +}; diff --git a/grpc-proxy/meson.build b/grpc-proxy/meson.build new file mode 100644 index 0000000..3d7171b --- /dev/null +++ b/grpc-proxy/meson.build @@ -0,0 +1,46 @@ +dep_wayland_client = dependency('wayland-client', version: '>= 1.17.0') + +grpcpp_reflection_dep = cxx.find_library('grpc++_reflection') +protoc = find_program('protoc') +grpc_cpp = find_program('grpc_cpp_plugin') + +protoc_gen = generator(protoc, \ + output : ['@BASENAME@.pb.cc', '@BASENAME@.pb.h'], + arguments : ['--proto_path=@CURRENT_SOURCE_DIR@/', + '--cpp_out=@BUILD_DIR@', + '@INPUT@']) + +generated_protoc_sources = protoc_gen.process('agl_shell.proto') + +grpc_gen = generator(protoc, \ + output : ['@BASENAME@.grpc.pb.cc', '@BASENAME@.grpc.pb.h'], + arguments : ['--proto_path=@CURRENT_SOURCE_DIR@/', + '--grpc_out=@BUILD_DIR@', + '--plugin=protoc-gen-grpc=' + grpc_cpp.path(), + '@INPUT@']) +generated_grpc_sources = grpc_gen.process('agl_shell.proto') + +grpc_deps = [ + dependency('protobuf'), + dependency('grpc'), + dependency('grpc++'), + grpcpp_reflection_dep, +] + +srcs = [ + 'main-grpc.cpp', + 'grpc-async-cb.cpp', + 'shell.cpp', + generated_protoc_sources, + generated_grpc_sources, + agl_shell_client_protocol_h, + agl_shell_protocol_c, +] + +executable( + 'agl-shell-grpc-server', srcs, + include_directories: [ common_inc ], + dependencies: [ grpc_deps, dep_wayland_client, libweston_dep ], + install: true, + install_dir: dir_module_agl_compositor, +) diff --git a/grpc-proxy/shell.cpp b/grpc-proxy/shell.cpp new file mode 100644 index 0000000..cc6ead6 --- /dev/null +++ b/grpc-proxy/shell.cpp @@ -0,0 +1,78 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <cstdio> +#include <ctime> +#include <algorithm> +#include <cstring> +#include <string> +#include <queue> + +#include "main-grpc.h" +#include "shell.h" + +void +Shell::ActivateApp(const std::string &app_id, const std::string &output_name) +{ + struct window_output *woutput, *w_output; + struct agl_shell *shell = this->m_shell.get(); + + woutput = nullptr; + w_output = nullptr; + + wl_list_for_each(woutput, &m_shell_data->output_list, link) { + if (woutput->name && !strcmp(woutput->name, output_name.c_str())) { + w_output = woutput; + break; + } + } + + // else, get the first one available + if (!w_output) + w_output = wl_container_of(m_shell_data->output_list.prev, + w_output, link); + + agl_shell_activate_app(shell, app_id.c_str(), w_output->output); + wl_display_flush(m_shell_data->wl_display); +} + +void +Shell::DeactivateApp(const std::string &app_id) +{ + (void) app_id; +} + +void +Shell::SetAppFloat(const std::string &app_id) +{ + (void) app_id; +} + +void +Shell::SetAppSplit(const std::string &app_id, uint32_t orientation) +{ + (void) app_id; + (void) orientation; +} diff --git a/grpc-proxy/shell.h b/grpc-proxy/shell.h new file mode 100644 index 0000000..03a4a87 --- /dev/null +++ b/grpc-proxy/shell.h @@ -0,0 +1,45 @@ +/* + * Copyright © 2022 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include <memory> + +#include "agl-shell-client-protocol.h" + +#include "main-grpc.h" + +class Shell { +public: + std::shared_ptr<struct agl_shell> m_shell; + struct shell_data *m_shell_data; + + Shell(std::shared_ptr<struct agl_shell> shell, + struct shell_data *sh_data) : + m_shell(shell), m_shell_data(sh_data) { } + void ActivateApp(const std::string &app_id, const std::string &output_name); + void DeactivateApp(const std::string &app_id); + void SetAppSplit(const std::string &app_id, uint32_t orientation); + void SetAppFloat(const std::string &app_id); +}; diff --git a/meson.build b/meson.build index 0958f06..26f70fd 100644 --- a/meson.build +++ b/meson.build @@ -296,3 +296,7 @@ install_data( common_inc = [ include_directories('src'), include_directories('.') ] subdir('clients') + +if get_option('grpc-proxy') + subdir('grpc-proxy') +endif diff --git a/meson_options.txt b/meson_options.txt index dd1f3c0..1e54773 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,10 @@ option( value: 'allow-all', description: 'Default policy when no specific policy was set' ) + +option( + 'grpc-proxy', + type: 'boolean', + value: true, + description: 'Build gRPC proxy which exposes some of the agl-shell protocol over a gRPC API' +) |