From 248f4847dfdc777d36879235db6f9096487abb40 Mon Sep 17 00:00:00 2001 From: Jente Hidskes Date: Tue, 30 Jun 2020 20:38:44 +0200 Subject: [PATCH] Start Cage 0.2 rewrite --- cageng.c | 328 +++++++++++++++++++++++++++++++++++++++++++++++ desktop/output.c | 184 ++++++++++++++++++++++++++ desktop/output.h | 34 +++++ meson.build | 38 ++---- serverng.h | 18 +++ 5 files changed, 577 insertions(+), 25 deletions(-) create mode 100644 cageng.c create mode 100644 desktop/output.c create mode 100644 desktop/output.h create mode 100644 serverng.h diff --git a/cageng.c b/cageng.c new file mode 100644 index 0000000..b4c9026 --- /dev/null +++ b/cageng.c @@ -0,0 +1,328 @@ +/* + * Cage: A Wayland kiosk. + * + * Copyright (C) 2018-2020 Jente Hidskes + * + * See the LICENSE file accompanying this file. + */ + +#define _POSIX_C_SOURCE 200112L + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "desktop/output.h" +#include "serverng.h" + +static int +sigchld_handler(int fd, uint32_t mask, void *user_data) +{ + struct wl_display *display = user_data; + + /* Close Cage's read pipe. */ + close(fd); + + if (mask & WL_EVENT_HANGUP) { + wlr_log(WLR_DEBUG, "Child process closed normally"); + } else if (mask & WL_EVENT_ERROR) { + wlr_log(WLR_DEBUG, "Connection closed by server"); + } + + wl_display_terminate(display); + return 0; +} + +static bool +set_cloexec(int fd) +{ + int flags = fcntl(fd, F_GETFD); + + if (flags == -1) { + wlr_log(WLR_ERROR, "Unable to set the CLOEXEC flag: fnctl failed"); + return false; + } + + flags = flags | FD_CLOEXEC; + if (fcntl(fd, F_SETFD, flags) == -1) { + wlr_log(WLR_ERROR, "Unable to set the CLOEXEC flag: fnctl failed"); + return false; + } + + return true; +} + +static bool +spawn_primary_client(struct wl_display *display, char *argv[], pid_t *pid_out, struct wl_event_source **sigchld_source) +{ + int fd[2]; + if (pipe(fd) != 0) { + wlr_log(WLR_ERROR, "Unable to create pipe"); + return false; + } + + pid_t pid = fork(); + if (pid == 0) { + sigset_t set; + sigemptyset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); + /* Close read, we only need write in the primary client process. */ + close(fd[0]); + execvp(argv[0], argv); + _exit(1); + } else if (pid == -1) { + wlr_log_errno(WLR_ERROR, "Unable to fork"); + return false; + } + + /* Set this early so that if we fail, the client process will be cleaned up properly. */ + *pid_out = pid; + + if (!set_cloexec(fd[0]) || !set_cloexec(fd[1])) { + return false; + } + + /* Close write, we only need read in Cage. */ + close(fd[1]); + + struct wl_event_loop *event_loop = wl_display_get_event_loop(display); + uint32_t mask = WL_EVENT_HANGUP | WL_EVENT_ERROR; + *sigchld_source = wl_event_loop_add_fd(event_loop, fd[0], mask, sigchld_handler, display); + + wlr_log(WLR_DEBUG, "Child process created with pid %d", pid); + return true; +} + +static void +cleanup_primary_client(pid_t pid) +{ + int status; + + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) { + wlr_log(WLR_DEBUG, "Child exited normally with exit status %d", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + wlr_log(WLR_DEBUG, "Child was terminated by a signal (%d)", WTERMSIG(status)); + } +} + +static bool +drop_permissions(void) +{ + if (getuid() != geteuid() || getgid() != getegid()) { + // Set the gid and uid in the correct order. + if (setgid(getgid()) != 0 || setuid(getuid()) != 0) { + wlr_log(WLR_ERROR, "Unable to drop root, refusing to start"); + return false; + } + } + + if (setgid(0) != -1 || setuid(0) != -1) { + wlr_log(WLR_ERROR, + "Unable to drop root (we shouldn't be able to restore it after setuid), refusing to start"); + return false; + } + + return true; +} + +static int +handle_signal(int signal, void *user_data) +{ + struct wl_display *display = user_data; + + switch (signal) { + case SIGINT: + /* Fallthrough */ + case SIGTERM: + wl_display_terminate(display); + return 0; + default: + return 0; + } +} + +static void +handle_new_output(struct wl_listener *listener, void *user_data) +{ + struct cg_server *server = wl_container_of(listener, server, new_output); + struct wlr_output *wlr_output = user_data; + + struct cg_output *output = calloc(1, sizeof(struct cg_output)); + if (!output) { + wlr_log(WLR_ERROR, "Failed to allocate output"); + return; + } + + wlr_output_layout_add_auto(server->output_layout, wlr_output); + + // TODO: do this before or after init? + wl_list_insert(&server->outputs, &output->link); + cage_output_init(output, wlr_output); +} + +static void +usage(FILE *file, const char *cage) +{ + fprintf(file, + "Usage: %s [OPTIONS] [--] APPLICATION\n" + "\n" + " -h\t Display this help message\n" + " -v\t Show the version number and exit\n" + "\n" + " Use -- when you want to pass arguments to APPLICATION\n", + cage); +} + +static bool +parse_args(struct cg_server *server, int argc, char *argv[]) +{ + int c; + while ((c = getopt(argc, argv, "hv")) != -1) { + switch (c) { + case 'h': + usage(stdout, argv[0]); + return false; + case 'v': + fprintf(stdout, "Cage version " CAGE_VERSION "\n"); + exit(0); + default: + usage(stderr, argv[0]); + return false; + } + } + + if (optind >= argc) { + usage(stderr, argv[0]); + return false; + } + + return true; +} + +int +main(int argc, char *argv[]) +{ + struct cg_server server = {0}; + struct wl_event_loop *event_loop = NULL; + struct wl_event_source *sigint_source = NULL; + struct wl_event_source *sigterm_source = NULL; + struct wl_event_source *sigchld_source = NULL; + struct wlr_backend *backend = NULL; + struct wlr_renderer *renderer = NULL; + struct wlr_compositor *compositor = NULL; + pid_t pid = 0; + int ret = 0; + + if (!parse_args(&server, argc, argv)) { + return 1; + } + +#ifdef DEBUG + wlr_log_init(WLR_DEBUG, NULL); +#else + wlr_log_init(WLR_ERROR, NULL); +#endif + + /* Wayland requires XDG_RUNTIME_DIR to be set. */ + if (!getenv("XDG_RUNTIME_DIR")) { + wlr_log(WLR_ERROR, "XDG_RUNTIME_DIR is not set in the environment"); + return 1; + } + + server.wl_display = wl_display_create(); + if (!server.wl_display) { + wlr_log(WLR_ERROR, "Cannot allocate a Wayland display"); + return 1; + } + + event_loop = wl_display_get_event_loop(server.wl_display); + sigint_source = wl_event_loop_add_signal(event_loop, SIGINT, handle_signal, &server.wl_display); + sigterm_source = wl_event_loop_add_signal(event_loop, SIGTERM, handle_signal, &server.wl_display); + + backend = wlr_backend_autocreate(server.wl_display, NULL); + if (!backend) { + wlr_log(WLR_ERROR, "Unable to create the wlroots backend"); + ret = 1; + goto end; + } + + if (!drop_permissions()) { + ret = 1; + goto end; + } + + renderer = wlr_backend_get_renderer(backend); + wlr_renderer_init_wl_display(renderer, server.wl_display); + + compositor = wlr_compositor_create(server.wl_display, renderer); + if (!compositor) { + wlr_log(WLR_ERROR, "Unable to create the wlroots compositor"); + ret = 1; + goto end; + } + + server.output_layout = wlr_output_layout_create(); + if (!server.output_layout) { + wlr_log(WLR_ERROR, "Unable to create output layout"); + ret = 1; + goto end; + } + + wl_list_init(&server.outputs); + server.new_output.notify = handle_new_output; + wl_signal_add(&backend->events.new_output, &server.new_output); + + const char *socket = wl_display_add_socket_auto(server.wl_display); + if (!socket) { + wlr_log_errno(WLR_ERROR, "Unable to open Wayland socket"); + ret = 1; + goto end; + } + + if (!wlr_backend_start(backend)) { + wlr_log(WLR_ERROR, "Unable to start the wlroots backend"); + ret = 1; + goto end; + } + + if (setenv("WAYLAND_DISPLAY", socket, true) < 0) { + wlr_log_errno(WLR_ERROR, "Unable to set WAYLAND_DISPLAY. Clients may not be able to connect"); + } else { + wlr_log(WLR_DEBUG, "Cage " CAGE_VERSION " is running on Wayland display %s", socket); + } + + if (!spawn_primary_client(server.wl_display, argv + optind, &pid, &sigchld_source)) { + ret = 1; + goto end; + } + + wl_display_run(server.wl_display); + wl_display_destroy_clients(server.wl_display); + +end: + cleanup_primary_client(pid); + + wl_event_source_remove(sigint_source); + wl_event_source_remove(sigterm_source); + if (sigchld_source) { + wl_event_source_remove(sigchld_source); + } + /* This function is not null-safe, but we only ever get here + with a proper wl_display. */ + wl_display_destroy(server.wl_display); + wlr_output_layout_destroy(server.output_layout); + return ret; +} diff --git a/desktop/output.c b/desktop/output.c new file mode 100644 index 0000000..ba58009 --- /dev/null +++ b/desktop/output.c @@ -0,0 +1,184 @@ +/* + * Cage: A Wayland kiosk. + * + * Copyright (C) 2018-2020 Jente Hidskes + * Copyright (C) 2019 The Sway authors + * + * See the LICENSE file accompanying this file. + */ + +#include +#include +#include +#include +#include +#if WLR_HAS_X11_BACKEND +#include +#endif +#include +#include +#include +#include +#include + +#include "output.h" + +static void +handle_output_damage_destroy(struct wl_listener *listener, void *user_data) +{ + struct cg_output *output = wl_container_of(listener, output, damage_destroy); + + if (output->wlr_output->enabled) { + cage_output_disable(output); + } + + wl_list_remove(&output->damage_destroy.link); +} + +static void +handle_output_transform(struct wl_listener *listener, void *user_data) +{ + struct cg_output *output = wl_container_of(listener, output, transform); + + assert(!output->wlr_output->enabled); + + // no-op +} + +static void +handle_output_mode(struct wl_listener *listener, void *user_data) +{ + struct cg_output *output = wl_container_of(listener, output, mode); + + assert(!output->wlr_output->enabled); + + // no-op +} + +static void +handle_output_destroy(struct wl_listener *listener, void *user_data) +{ + struct cg_output *output = wl_container_of(listener, output, destroy); + cage_output_fini(output); +} + +void +cage_output_get_geometry(struct cg_output *output, struct wlr_box *geometry) +{ + assert(output != NULL); + assert(geometry != NULL); + + wlr_output_effective_resolution(output->wlr_output, &geometry->width, &geometry->height); +} + +void +cage_output_disable(struct cg_output *output) +{ + assert(output != NULL); + assert(output->wlr_output->enabled); + + struct wlr_output *wlr_output = output->wlr_output; + + wlr_log(WLR_DEBUG, "Disabling output %s", wlr_output->name); + + wl_list_remove(&output->mode.link); + wl_list_init(&output->mode.link); + wl_list_remove(&output->transform.link); + wl_list_init(&output->transform.link); + wl_list_remove(&output->damage_frame.link); + wl_list_init(&output->damage_frame.link); + + wlr_output_rollback(wlr_output); + wlr_output_enable(wlr_output, false); + wlr_output_commit(wlr_output); +} + +void +cage_output_enable(struct cg_output *output) +{ + assert(output != NULL); + /* Outputs get enabled by the backend before firing the new_output event, + * so we can't do a check for already enabled outputs here unless we + * duplicate the enabled property in cg_output. */ + + struct wlr_output *wlr_output = output->wlr_output; + + wlr_log(WLR_DEBUG, "Enabling output %s", wlr_output->name); + + wl_list_remove(&output->mode.link); + output->mode.notify = handle_output_mode; + wl_signal_add(&wlr_output->events.mode, &output->mode); + wl_list_remove(&output->transform.link); + output->transform.notify = handle_output_transform; + wl_signal_add(&wlr_output->events.transform, &output->transform); + wl_list_remove(&output->damage_frame.link); + // output->damage_frame.notify = handle_output_damage_frame; + wl_signal_add(&output->damage->events.frame, &output->damage_frame); + + wlr_output_enable(wlr_output, true); + wlr_output_commit(wlr_output); + wlr_output_damage_add_whole(output->damage); +} + +void +cage_output_init(struct cg_output *output, struct wlr_output *wlr_output) +{ + assert(output != NULL); + assert(wlr_output != NULL); + + output->wlr_output = wlr_output; + wlr_output->data = output; + output->damage = wlr_output_damage_create(wlr_output); + wl_list_init(&output->views); + + /* We need to init the lists here because cage_output_enable calls + * `wl_list_remove` on these. */ + wl_list_init(&output->mode.link); + wl_list_init(&output->transform.link); + wl_list_init(&output->damage_frame.link); + + output->destroy.notify = handle_output_destroy; + wl_signal_add(&wlr_output->events.destroy, &output->destroy); + output->damage_destroy.notify = handle_output_damage_destroy; + wl_signal_add(&output->damage->events.destroy, &output->damage_destroy); + + struct wlr_output_mode *preferred_mode = wlr_output_preferred_mode(wlr_output); + if (preferred_mode) { + wlr_output_set_mode(wlr_output, preferred_mode); + } + + cage_output_enable(output); +} + +void +cage_output_fini(struct cg_output *output) +{ + assert(output != NULL); + + if (output->wlr_output->enabled) { + cage_output_disable(output); + } + + wl_list_remove(&output->destroy.link); + wl_list_remove(&output->link); + + free(output); +} + +void +cage_output_set_window_title(struct cg_output *output, const char *title) +{ + assert(output != NULL); + assert(title != NULL); + assert(output->wlr_output->enabled); + + struct wlr_output *wlr_output = output->wlr_output; + + if (wlr_output_is_wl(wlr_output)) { + wlr_wl_output_set_title(wlr_output, title); +#if WLR_HAS_X11_BACKEND + } else if (wlr_output_is_x11(wlr_output)) { + wlr_x11_output_set_title(wlr_output, title); +#endif + } +} diff --git a/desktop/output.h b/desktop/output.h new file mode 100644 index 0000000..ff54723 --- /dev/null +++ b/desktop/output.h @@ -0,0 +1,34 @@ +#ifndef CG_OUTPUT_H +#define CG_OUTPUT_H + +#include +#include +#include +#include + +struct cg_output { + struct wlr_output *wlr_output; + struct wlr_output_damage *damage; + + /** + * The views on this output. Ordered from top to bottom. + */ + struct wl_list views; + + struct wl_listener mode; + struct wl_listener transform; + struct wl_listener destroy; + struct wl_listener damage_frame; + struct wl_listener damage_destroy; + + struct wl_list link; // cg_server::outputs +}; + +void cage_output_get_geometry(struct cg_output *output, struct wlr_box *geometry); +void cage_output_disable(struct cg_output *output); +void cage_output_enable(struct cg_output *output); +void cage_output_init(struct cg_output *output, struct wlr_output *wlr_output); +void cage_output_fini(struct cg_output *output); +void cage_output_set_window_title(struct cg_output *output, const char *title); + +#endif diff --git a/meson.build b/meson.build index e7e12b8..33b882d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cage', 'c', - version: '0.1.2', + version: '0.2.0', license: 'MIT', default_options: [ 'c_std=c11', @@ -119,39 +119,27 @@ if scdoc.found() endforeach endif -cage_sources = [ - 'cage.c', - 'idle_inhibit_v1.c', - 'output.c', - 'render.c', - 'seat.c', - 'util.c', - 'view.c', - 'xdg_shell.c', +cageng_sources = [ + 'desktop/output.c', + 'cageng.c', ] -cage_headers = [ +cageng_headers = [ configure_file(input: 'config.h.in', output: 'config.h', configuration: conf_data), - 'idle_inhibit_v1.h', - 'output.h', - 'render.h', - 'seat.h', - 'server.h', - 'util.h', - 'view.h', - 'xdg_shell.h', + 'desktop/output.h', + 'serverng.h', ] -if conf_data.get('CAGE_HAS_XWAYLAND', 0) == 1 - cage_sources += 'xwayland.c' - cage_headers += 'xwayland.h' -endif +# if conf_data.get('CAGE_HAS_XWAYLAND', 0) == 1 +# cage_sources += 'xwayland.c' +# cage_headers += 'xwayland.h' +# endif executable( - meson.project_name(), - cage_sources + cage_headers, + meson.project_name() + 'ng', + cageng_sources + cageng_headers, dependencies: [ server_protos, wayland_server, diff --git a/serverng.h b/serverng.h new file mode 100644 index 0000000..16a25d2 --- /dev/null +++ b/serverng.h @@ -0,0 +1,18 @@ +#ifndef CG_SERVER_H +#define CG_SERVER_H + +#include +#include + +#include "desktop/output.h" + +struct cg_server { + struct wl_display *wl_display; + + /* Includes disabled outputs. */ + struct wl_list outputs; // cg_output::link + struct wlr_output_layout *output_layout; + struct wl_listener new_output; +}; + +#endif