/* * Cage: A Wayland kiosk. * * Copyright (C) 2018-2021 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 #include #include #include #include #include #include #include #include #include #include #include #if CAGE_HAS_XWAYLAND #include #endif #include #include #include #include #if CAGE_HAS_XWAYLAND #include #endif #include "idle_inhibit_v1.h" #include "layer_shell_v1.h" #include "output.h" #include "seat.h" #include "server.h" #include "view.h" #include "xdg_shell.h" #if CAGE_HAS_XWAYLAND #include "xwayland.h" #endif static int sigchld_handler(int fd, uint32_t mask, void *data) { struct wl_display *display = 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 *data) { struct wl_display *display = data; switch (signal) { case SIGINT: /* Fallthrough */ case SIGTERM: wl_display_terminate(display); return 0; default: return 0; } } static void usage(FILE *file, const char *cage) { fprintf(file, "Usage: %s [OPTIONS] [--] APPLICATION\n" "\n" " -d\t Don't draw client side decorations, when possible\n" " -h\t Display this help message\n" " -m extend Extend the display across all connected outputs (default)\n" " -m last Use only the last connected output\n" " -r\t Rotate the output 90 degrees clockwise, specify up to three times\n" " -s\t Allow VT switching\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, "dhm:rsv")) != -1) { switch (c) { case 'd': server->xdg_decoration = true; break; case 'h': usage(stdout, argv[0]); return false; case 'm': if (strcmp(optarg, "last") == 0) { server->output_mode = CAGE_MULTI_OUTPUT_MODE_LAST; } else if (strcmp(optarg, "extend") == 0) { server->output_mode = CAGE_MULTI_OUTPUT_MODE_EXTEND; } break; case 'r': server->output_transform++; if (server->output_transform > WL_OUTPUT_TRANSFORM_270) { server->output_transform = WL_OUTPUT_TRANSFORM_NORMAL; } break; case 's': server->allow_vt_switch = true; break; 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_compositor *compositor = NULL; struct wlr_data_device_manager *data_device_manager = NULL; struct wlr_server_decoration_manager *server_decoration_manager = NULL; struct wlr_xdg_decoration_manager_v1 *xdg_decoration_manager = NULL; struct wlr_export_dmabuf_manager_v1 *export_dmabuf_manager = NULL; struct wlr_screencopy_manager_v1 *screencopy_manager = NULL; struct wlr_xdg_output_manager_v1 *output_manager = NULL; struct wlr_gamma_control_manager_v1 *gamma_control_manager = NULL; struct wlr_viewporter *viewporter = NULL; struct wlr_presentation *presentation = NULL; struct wlr_xdg_shell *xdg_shell = NULL; #if CAGE_HAS_XWAYLAND struct wlr_xwayland *xwayland = NULL; struct wlr_xcursor_manager *xcursor_manager = NULL; #endif 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); server.backend = wlr_backend_autocreate(server.wl_display); if (!server.backend) { wlr_log(WLR_ERROR, "Unable to create the wlroots backend"); ret = 1; goto end; } if (!drop_permissions()) { ret = 1; goto end; } server.renderer = wlr_renderer_autocreate(server.backend); if (!server.renderer) { wlr_log(WLR_ERROR, "Unable to create the wlroots renderer"); ret = 1; goto end; } server.allocator = wlr_allocator_autocreate(server.backend, server.renderer); if (!server.allocator) { wlr_log(WLR_ERROR, "Unable to create the wlroots allocator"); ret = 1; goto end; } wlr_renderer_init_wl_display(server.renderer, server.wl_display); wl_list_init(&server.views); wl_list_init(&server.outputs); server.output_layout = wlr_output_layout_create(); if (!server.output_layout) { wlr_log(WLR_ERROR, "Unable to create output layout"); ret = 1; goto end; } server.scene = wlr_scene_create(); if (!server.scene) { wlr_log(WLR_ERROR, "Unable to create scene"); ret = 1; goto end; } wlr_scene_attach_output_layout(server.scene, server.output_layout); compositor = wlr_compositor_create(server.wl_display, server.renderer); if (!compositor) { wlr_log(WLR_ERROR, "Unable to create the wlroots compositor"); ret = 1; goto end; } data_device_manager = wlr_data_device_manager_create(server.wl_display); if (!data_device_manager) { wlr_log(WLR_ERROR, "Unable to create the data device manager"); ret = 1; goto end; } /* Configure a listener to be notified when new outputs are * available on the backend. We use this only to detect the * first output and ignore subsequent outputs. */ server.new_output.notify = handle_new_output; wl_signal_add(&server.backend->events.new_output, &server.new_output); server.seat = seat_create(&server, server.backend); if (!server.seat) { wlr_log(WLR_ERROR, "Unable to create the seat"); ret = 1; goto end; } server.idle = wlr_idle_create(server.wl_display); if (!server.idle) { wlr_log(WLR_ERROR, "Unable to create the idle tracker"); ret = 1; goto end; } server.idle_inhibit_v1 = wlr_idle_inhibit_v1_create(server.wl_display); if (!server.idle_inhibit_v1) { wlr_log(WLR_ERROR, "Cannot create the idle inhibitor"); ret = 1; goto end; } server.new_idle_inhibitor_v1.notify = handle_idle_inhibitor_v1_new; wl_signal_add(&server.idle_inhibit_v1->events.new_inhibitor, &server.new_idle_inhibitor_v1); wl_list_init(&server.inhibitors); xdg_shell = wlr_xdg_shell_create(server.wl_display); if (!xdg_shell) { wlr_log(WLR_ERROR, "Unable to create the XDG shell interface"); ret = 1; goto end; } server.new_xdg_shell_surface.notify = handle_xdg_shell_surface_new; wl_signal_add(&xdg_shell->events.new_surface, &server.new_xdg_shell_surface); xdg_decoration_manager = wlr_xdg_decoration_manager_v1_create(server.wl_display); if (!xdg_decoration_manager) { wlr_log(WLR_ERROR, "Unable to create the XDG decoration manager"); ret = 1; goto end; } wl_signal_add(&xdg_decoration_manager->events.new_toplevel_decoration, &server.xdg_toplevel_decoration); server.xdg_toplevel_decoration.notify = handle_xdg_toplevel_decoration; server_decoration_manager = wlr_server_decoration_manager_create(server.wl_display); if (!server_decoration_manager) { wlr_log(WLR_ERROR, "Unable to create the server decoration manager"); ret = 1; goto end; } wlr_server_decoration_manager_set_default_mode( server_decoration_manager, server.xdg_decoration ? WLR_SERVER_DECORATION_MANAGER_MODE_SERVER : WLR_SERVER_DECORATION_MANAGER_MODE_CLIENT); viewporter = wlr_viewporter_create(server.wl_display); if (!viewporter) { wlr_log(WLR_ERROR, "Unable to create the viewporter interface"); ret = 1; goto end; } presentation = wlr_presentation_create(server.wl_display, server.backend); if (!presentation) { wlr_log(WLR_ERROR, "Unable to create the presentation interface"); ret = 1; goto end; } wlr_scene_set_presentation(server.scene, presentation); server.layer_shell_v1 = wlr_layer_shell_v1_create(server.wl_display); wl_signal_add(&server.layer_shell_v1->events.new_surface, &server.new_layer_shell_v1_surface); server.new_layer_shell_v1_surface.notify = handle_layer_shell_v1_surface_new; export_dmabuf_manager = wlr_export_dmabuf_manager_v1_create(server.wl_display); if (!export_dmabuf_manager) { wlr_log(WLR_ERROR, "Unable to create the export DMABUF manager"); ret = 1; goto end; } screencopy_manager = wlr_screencopy_manager_v1_create(server.wl_display); if (!screencopy_manager) { wlr_log(WLR_ERROR, "Unable to create the screencopy manager"); ret = 1; goto end; } output_manager = wlr_xdg_output_manager_v1_create(server.wl_display, server.output_layout); if (!output_manager) { wlr_log(WLR_ERROR, "Unable to create the output manager"); ret = 1; goto end; } gamma_control_manager = wlr_gamma_control_manager_v1_create(server.wl_display); if (!gamma_control_manager) { wlr_log(WLR_ERROR, "Unable to create the gamma control manager"); ret = 1; goto end; } #if CAGE_HAS_XWAYLAND xwayland = wlr_xwayland_create(server.wl_display, compositor, true); if (!xwayland) { wlr_log(WLR_ERROR, "Cannot create XWayland server"); ret = 1; goto end; } server.new_xwayland_surface.notify = handle_xwayland_surface_new; wl_signal_add(&xwayland->events.new_surface, &server.new_xwayland_surface); xcursor_manager = wlr_xcursor_manager_create(DEFAULT_XCURSOR, XCURSOR_SIZE); if (!xcursor_manager) { wlr_log(WLR_ERROR, "Cannot create XWayland XCursor manager"); ret = 1; goto end; } if (setenv("DISPLAY", xwayland->display_name, true) < 0) { wlr_log_errno(WLR_ERROR, "Unable to set DISPLAY for XWayland. Clients may not be able to connect"); } else { wlr_log(WLR_DEBUG, "XWayland is running on display %s", xwayland->display_name); } if (!wlr_xcursor_manager_load(xcursor_manager, 1)) { wlr_log(WLR_ERROR, "Cannot load XWayland XCursor theme"); } struct wlr_xcursor *xcursor = wlr_xcursor_manager_get_xcursor(xcursor_manager, DEFAULT_XCURSOR, 1); if (xcursor) { struct wlr_xcursor_image *image = xcursor->images[0]; wlr_xwayland_set_cursor(xwayland, image->buffer, image->width * 4, image->width, image->height, image->hotspot_x, image->hotspot_y); } #endif 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(server.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 CAGE_HAS_XWAYLAND wlr_xwayland_set_seat(xwayland, server.seat->seat); #endif if (!spawn_primary_client(server.wl_display, argv + optind, &pid, &sigchld_source)) { ret = 1; goto end; } /* Place the cursor in the center of the output layout. */ struct wlr_box *layout_box = wlr_output_layout_get_box(server.output_layout, NULL); wlr_cursor_warp(server.seat->cursor, NULL, layout_box->width / 2, layout_box->height / 2); wl_display_run(server.wl_display); #if CAGE_HAS_XWAYLAND wlr_xwayland_destroy(xwayland); wlr_xcursor_manager_destroy(xcursor_manager); #endif 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); } seat_destroy(server.seat); /* 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; }