Files
river/river/Server.zig
2026-03-30 10:17:51 +02:00

629 lines
25 KiB
Zig

// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Server = @This();
const build_options = @import("build_options");
const std = @import("std");
const assert = std.debug.assert;
const mem = std.mem;
const posix = std.posix;
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const wp = wayland.server.wp;
const c = @import("c.zig").c;
const util = @import("util.zig");
const Config = @import("Config.zig");
const Control = @import("Control.zig");
const IdleInhibitManager = @import("IdleInhibitManager.zig");
const InputManager = @import("InputManager.zig");
const LayerSurface = @import("LayerSurface.zig");
const LayoutManager = @import("LayoutManager.zig");
const LockManager = @import("LockManager.zig");
const Output = @import("Output.zig");
const Root = @import("Root.zig");
const Seat = @import("Seat.zig");
const SceneNodeData = @import("SceneNodeData.zig");
const StatusManager = @import("StatusManager.zig");
const TabletTool = @import("TabletTool.zig");
const XdgDecoration = @import("XdgDecoration.zig");
const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const XwaylandView = @import("XwaylandView.zig");
const View = @import("View.zig");
const log = std.log.scoped(.server);
wl_server: *wl.Server,
sigint_source: *wl.EventSource,
sigterm_source: *wl.EventSource,
fixes: *wlr.Fixes,
backend: *wlr.Backend,
session: ?*wlr.Session,
renderer: *wlr.Renderer,
allocator: *wlr.Allocator,
gpu_reset_recover: ?*wl.EventSource = null,
security_context_manager: *wlr.SecurityContextManagerV1,
shm: *wlr.Shm,
linux_dmabuf: ?*wlr.LinuxDmabufV1 = null,
linux_drm_syncobj_manager: ?*wlr.LinuxDrmSyncobjManagerV1 = null,
single_pixel_buffer_manager: *wlr.SinglePixelBufferManagerV1,
color_manager: ?*wlr.ColorManagerV1 = null,
color_representation_manager: *wlr.ColorRepresentationManagerV1,
viewporter: *wlr.Viewporter,
fractional_scale_manager: *wlr.FractionalScaleManagerV1,
compositor: *wlr.Compositor,
subcompositor: *wlr.Subcompositor,
cursor_shape_manager: *wlr.CursorShapeManagerV1,
xdg_shell: *wlr.XdgShell,
xdg_decoration_manager: *wlr.XdgDecorationManagerV1,
layer_shell: *wlr.LayerShellV1,
xdg_activation: *wlr.XdgActivationV1,
data_device_manager: *wlr.DataDeviceManager,
primary_selection_manager: *wlr.PrimarySelectionDeviceManagerV1,
data_control_manager: *wlr.DataControlManagerV1,
export_dmabuf_manager: *wlr.ExportDmabufManagerV1,
screencopy_manager: *wlr.ScreencopyManagerV1,
image_copy_capture_manager: *wlr.ExtImageCopyCaptureManagerV1,
output_image_capture_source_manager: *wlr.ExtOutputImageCaptureSourceManagerV1,
foreign_toplevel_image_capture_source_manager: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1,
foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
foreign_toplevel_list: *wlr.ExtForeignToplevelListV1,
tearing_control_manager: *wlr.TearingControlManagerV1,
alpha_modifier: *wlr.AlphaModifierV1,
input_manager: InputManager,
root: Root,
config: Config,
control: Control,
status_manager: StatusManager,
layout_manager: LayoutManager,
idle_inhibit_manager: IdleInhibitManager,
lock_manager: LockManager,
xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_options.xwayland) null,
new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void =
if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface).init(handleNewXwaylandSurface),
renderer_lost: wl.Listener(void) = wl.Listener(void).init(handleRendererLost),
new_xdg_toplevel: wl.Listener(*wlr.XdgToplevel) =
wl.Listener(*wlr.XdgToplevel).init(handleNewXdgToplevel),
new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) =
wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleNewToplevelDecoration),
new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1) =
wl.Listener(*wlr.LayerSurfaceV1).init(handleNewLayerSurface),
request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) =
wl.Listener(*wlr.XdgActivationV1.event.RequestActivate).init(handleRequestActivate),
request_set_cursor_shape: wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape) =
wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape).init(handleRequestSetCursorShape),
new_foreign_toplevel_capture_request: wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request) =
wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request).init(handleNewForeignToplevelCaptureRequest),
pub fn init(server: *Server, runtime_xwayland: bool) !void {
// We intentionally don't try to prevent memory leaks on error in this function
// since river will exit during initialization anyway if there is an error.
// This keeps the code simpler and more readable.
const wl_server = try wl.Server.create();
const loop = wl_server.getEventLoop();
var session: ?*wlr.Session = undefined;
const backend = try wlr.Backend.autocreate(loop, &session);
const renderer = try wlr.Renderer.autocreate(backend);
const compositor = try wlr.Compositor.create(wl_server, 6, renderer);
server.* = .{
.wl_server = wl_server,
.sigint_source = try loop.addSignal(*wl.Server, posix.SIG.INT, terminate, wl_server),
.sigterm_source = try loop.addSignal(*wl.Server, posix.SIG.TERM, terminate, wl_server),
.fixes = try wlr.Fixes.create(wl_server, 1),
.backend = backend,
.session = session,
.renderer = renderer,
.allocator = try wlr.Allocator.autocreate(backend, renderer),
.security_context_manager = try wlr.SecurityContextManagerV1.create(wl_server),
.shm = try wlr.Shm.createWithRenderer(wl_server, 2, renderer),
.single_pixel_buffer_manager = try wlr.SinglePixelBufferManagerV1.create(wl_server),
.color_representation_manager = try wlr.ColorRepresentationManagerV1.createWithRenderer(wl_server, 1, renderer),
.viewporter = try wlr.Viewporter.create(wl_server),
.fractional_scale_manager = try wlr.FractionalScaleManagerV1.create(wl_server, 1),
.compositor = compositor,
.subcompositor = try wlr.Subcompositor.create(wl_server),
.cursor_shape_manager = try wlr.CursorShapeManagerV1.create(server.wl_server, 2),
.xdg_shell = try wlr.XdgShell.create(wl_server, 5),
.xdg_decoration_manager = try wlr.XdgDecorationManagerV1.create(wl_server),
.layer_shell = try wlr.LayerShellV1.create(wl_server, 4),
.xdg_activation = try wlr.XdgActivationV1.create(wl_server),
.data_device_manager = try wlr.DataDeviceManager.create(wl_server),
.primary_selection_manager = try wlr.PrimarySelectionDeviceManagerV1.create(wl_server),
.data_control_manager = try wlr.DataControlManagerV1.create(wl_server),
.export_dmabuf_manager = try wlr.ExportDmabufManagerV1.create(wl_server),
.screencopy_manager = try wlr.ScreencopyManagerV1.create(wl_server),
.image_copy_capture_manager = try wlr.ExtImageCopyCaptureManagerV1.create(wl_server, 1),
.output_image_capture_source_manager = try wlr.ExtOutputImageCaptureSourceManagerV1.create(wl_server, 1),
.foreign_toplevel_image_capture_source_manager = try wlr.ExtForeignToplevelImageCaptureSourceManagerV1.create(wl_server, 1),
.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(wl_server),
.foreign_toplevel_list = try wlr.ExtForeignToplevelListV1.create(wl_server, 1),
.tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1),
.alpha_modifier = try wlr.AlphaModifierV1.create(wl_server),
.config = try Config.init(),
.root = undefined,
.input_manager = undefined,
.control = undefined,
.status_manager = undefined,
.layout_manager = undefined,
.idle_inhibit_manager = undefined,
.lock_manager = undefined,
};
if (renderer.getTextureFormats(@intFromEnum(wlr.BufferCap.dmabuf)) != null) {
server.linux_dmabuf = try wlr.LinuxDmabufV1.createWithRenderer(wl_server, 4, renderer);
}
if (renderer.features.timeline and backend.features.timeline) {
const drm_fd = renderer.getDrmFd();
if (drm_fd >= 0) {
server.linux_drm_syncobj_manager = wlr.LinuxDrmSyncobjManagerV1.create(wl_server, 1, drm_fd);
}
}
if (renderer.features.input_color_transform) {
const render_intents: []const wp.ColorManagerV1.RenderIntent = &.{.perceptual};
const transfer_functions = renderer.transferFunctionList();
defer std.c.free(transfer_functions.ptr);
const primaries = renderer.primariesList();
defer std.c.free(primaries.ptr);
server.color_manager = try wlr.ColorManagerV1.create(wl_server, 2, .{
.features = .{
.parametric = true,
.set_mastering_display_primaries = true,
},
.render_intents = render_intents,
.transfer_functions = transfer_functions,
.primaries = primaries,
});
}
if (build_options.xwayland and runtime_xwayland) {
server.xwayland = try wlr.Xwayland.create(wl_server, compositor, false);
server.xwayland.?.events.new_surface.add(&server.new_xwayland_surface);
}
try server.root.init();
try server.input_manager.init();
try server.control.init();
try server.status_manager.init();
try server.layout_manager.init();
try server.idle_inhibit_manager.init();
try server.lock_manager.init();
server.renderer.events.lost.add(&server.renderer_lost);
server.xdg_shell.events.new_toplevel.add(&server.new_xdg_toplevel);
server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration);
server.layer_shell.events.new_surface.add(&server.new_layer_surface);
server.xdg_activation.events.request_activate.add(&server.request_activate);
server.cursor_shape_manager.events.request_set_shape.add(&server.request_set_cursor_shape);
server.foreign_toplevel_image_capture_source_manager.events.new_request.add(&server.new_foreign_toplevel_capture_request);
wl_server.setGlobalFilter(*Server, globalFilter, server);
}
/// Free allocated memory and clean up. Note: order is important here
pub fn deinit(server: *Server) void {
server.sigint_source.remove();
server.sigterm_source.remove();
server.renderer_lost.link.remove();
server.new_xdg_toplevel.link.remove();
server.new_toplevel_decoration.link.remove();
server.new_layer_surface.link.remove();
server.request_activate.link.remove();
server.request_set_cursor_shape.link.remove();
server.new_foreign_toplevel_capture_request.link.remove();
if (build_options.xwayland) {
if (server.xwayland) |xwayland| {
server.new_xwayland_surface.link.remove();
xwayland.destroy();
}
}
server.wl_server.destroyClients();
server.input_manager.new_input.link.remove();
server.root.new_output.link.remove();
server.backend.destroy();
// The scene graph needs to be destroyed after the backend but before the renderer
// Output destruction requires the scene graph to still be around while the scene
// graph may require the renderer to still be around to destroy textures it seems.
server.root.scene.tree.node.destroy();
server.renderer.destroy();
server.allocator.destroy();
server.root.deinit();
server.input_manager.deinit();
server.idle_inhibit_manager.deinit();
server.lock_manager.deinit();
server.wl_server.destroy();
server.config.deinit();
}
/// Create the socket, start the backend, and setup the environment
pub fn start(server: Server) !void {
var buf: [11]u8 = undefined;
const socket = try server.wl_server.addSocketAuto(&buf);
try server.backend.start();
// TODO: don't use libc's setenv
if (c.setenv("WAYLAND_DISPLAY", socket.ptr, 1) < 0) return error.SetenvError;
if (build_options.xwayland) {
if (server.xwayland) |xwayland| {
if (c.setenv("DISPLAY", xwayland.display_name, 1) < 0) return error.SetenvError;
}
}
}
fn globalFilter(client: *const wl.Client, global: *const wl.Global, server: *Server) bool {
// Only expose the xwalyand_shell_v1 global to the Xwayland process.
if (build_options.xwayland) {
if (server.xwayland) |xwayland| {
if (global == xwayland.shell_v1.global) {
if (xwayland.server) |xwayland_server| {
return client == xwayland_server.client;
}
return false;
}
}
}
// User-configurable allow/block lists are TODO
if (server.security_context_manager.lookupClient(client) != null) {
const allowed = server.allowlist(global);
const blocked = server.blocklist(global);
assert(allowed != blocked);
return allowed;
} else {
return true;
}
}
/// Returns true if the global is allowlisted for security contexts
fn allowlist(server: *Server, global: *const wl.Global) bool {
if (server.linux_dmabuf) |linux_dmabuf| {
if (global == linux_dmabuf.global) return true;
}
if (server.linux_drm_syncobj_manager) |linux_drm_syncobj_manager| {
if (global == linux_drm_syncobj_manager.global) return true;
}
if (server.color_manager) |color_manager| {
if (global == color_manager.global) return true;
}
// We must use the getInterface() approach for dynamically created globals
// such as wl_output and wl_seat since the wl_global_create() function will
// advertise the global to clients and invoke this filter before returning
// the new global pointer.
if ((mem.orderZ(u8, global.getInterface().name, "wl_output") == .eq) or
(mem.orderZ(u8, global.getInterface().name, "wl_seat") == .eq))
{
return true;
}
// For other globals I like the current pointer comparison approach as it
// should catch river accidentally exposing multiple copies of e.g. wl_shm
// with an assertion failure.
return global == server.fixes.global or
global == server.shm.global or
global == server.single_pixel_buffer_manager.global or
global == server.color_representation_manager.global or
global == server.viewporter.global or
global == server.fractional_scale_manager.global or
global == server.compositor.global or
global == server.subcompositor.global or
global == server.cursor_shape_manager.global or
global == server.xdg_shell.global or
global == server.xdg_decoration_manager.global or
global == server.xdg_activation.global or
global == server.data_device_manager.global or
global == server.primary_selection_manager.global or
global == server.root.presentation.global or
global == server.root.xdg_output_manager.global or
global == server.input_manager.relative_pointer_manager.global or
global == server.input_manager.pointer_constraints.global or
global == server.input_manager.text_input_manager.global or
global == server.input_manager.tablet_manager.global or
global == server.input_manager.pointer_gestures.global or
global == server.idle_inhibit_manager.wlr_manager.global or
global == server.tearing_control_manager.global or
global == server.alpha_modifier.global;
}
/// Returns true if the global is blocked for security contexts
fn blocklist(server: *Server, global: *const wl.Global) bool {
return global == server.security_context_manager.global or
global == server.layer_shell.global or
global == server.foreign_toplevel_manager.global or
global == server.foreign_toplevel_list.global or
global == server.screencopy_manager.global or
global == server.image_copy_capture_manager.global or
global == server.output_image_capture_source_manager.global or
global == server.foreign_toplevel_image_capture_source_manager.global or
global == server.export_dmabuf_manager.global or
global == server.data_control_manager.global or
global == server.layout_manager.global or
global == server.control.global or
global == server.status_manager.global or
global == server.root.output_manager.global or
global == server.root.power_manager.global or
global == server.root.gamma_control_manager.global or
global == server.input_manager.idle_notifier.global or
global == server.input_manager.virtual_pointer_manager.global or
global == server.input_manager.virtual_keyboard_manager.global or
global == server.input_manager.input_method_manager.global or
global == server.lock_manager.wlr_manager.global;
}
/// Handle SIGINT and SIGTERM by gracefully stopping the server
fn terminate(_: c_int, wl_server: *wl.Server) c_int {
wl_server.terminate();
return 0;
}
fn handleRendererLost(listener: *wl.Listener(void)) void {
const server: *Server = @fieldParentPtr("renderer_lost", listener);
if (server.gpu_reset_recover != null) {
log.info("ignoring GPU reset event, recovery already scheduled", .{});
return;
}
log.info("received GPU reset event, scheduling recovery", .{});
// There's a design wart in this wlroots API: calling wlr_renderer_destroy()
// from inside this listener for the renderer lost event causes the assertion
// that all listener lists are empty in wlr_renderer_destroy() to fail. This
// happens even if river has already called server.renderer_lost.link.remove()
// since wlroots uses wl_signal_emit_mutable(), which is implemented by adding
// temporary links to the list during iteration.
// Using an idle callback is the most straightforward way to work around this
// design wart.
const event_loop = server.wl_server.getEventLoop();
server.gpu_reset_recover = event_loop.addIdle(*Server, gpuResetRecoverIdle, server) catch |err| switch (err) {
error.OutOfMemory => {
log.err("out of memory", .{});
return;
},
};
}
fn gpuResetRecoverIdle(server: *Server) void {
server.gpu_reset_recover = null;
// There's not much that can be done if creating a new renderer or allocator fails.
// With luck there might be another GPU reset after which we try again and succeed.
server.gpuResetRecover() catch |err| switch (err) {
error.RendererCreateFailed => log.err("failed to create new renderer after GPU reset", .{}),
error.AllocatorCreateFailed => log.err("failed to create new allocator after GPU reset", .{}),
};
}
fn gpuResetRecover(server: *Server) !void {
log.info("recovering from GPU reset", .{});
const new_renderer = try wlr.Renderer.autocreate(server.backend);
errdefer new_renderer.destroy();
const new_allocator = try wlr.Allocator.autocreate(server.backend, new_renderer);
errdefer comptime unreachable; // no failure allowed after this point
server.renderer_lost.link.remove();
new_renderer.events.lost.add(&server.renderer_lost);
server.compositor.setRenderer(new_renderer);
{
var it = server.root.all_outputs.iterator(.forward);
while (it.next()) |output| {
// This should never fail here as failure with this combination of
// renderer, allocator, and backend should have prevented creating
// the output in the first place.
_ = output.wlr_output.initRender(new_allocator, new_renderer);
}
}
server.renderer.destroy();
server.renderer = new_renderer;
server.allocator.destroy();
server.allocator = new_allocator;
}
fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void {
log.debug("new xdg_toplevel", .{});
XdgToplevel.create(xdg_toplevel) catch {
log.err("out of memory", .{});
xdg_toplevel.resource.postNoMemory();
return;
};
}
fn handleNewToplevelDecoration(
_: *wl.Listener(*wlr.XdgToplevelDecorationV1),
wlr_decoration: *wlr.XdgToplevelDecorationV1,
) void {
XdgDecoration.init(wlr_decoration);
}
fn handleNewLayerSurface(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void {
const server: *Server = @fieldParentPtr("new_layer_surface", listener);
log.debug(
"new layer surface: namespace {s}, layer {s}, anchor {b:0>4}, size {},{}, margin {},{},{},{}, exclusive_zone {}",
.{
wlr_layer_surface.namespace,
@tagName(wlr_layer_surface.current.layer),
@as(u32, @bitCast(wlr_layer_surface.current.anchor)),
wlr_layer_surface.current.desired_width,
wlr_layer_surface.current.desired_height,
wlr_layer_surface.current.margin.top,
wlr_layer_surface.current.margin.right,
wlr_layer_surface.current.margin.bottom,
wlr_layer_surface.current.margin.left,
wlr_layer_surface.current.exclusive_zone,
},
);
// If the new layer surface does not have an output assigned to it, use the
// first output or close the surface if none are available.
if (wlr_layer_surface.output == null) {
const output = server.input_manager.defaultSeat().focused_output orelse {
log.err("no output available for layer surface '{s}'", .{wlr_layer_surface.namespace});
wlr_layer_surface.destroy();
return;
};
log.debug("new layer surface had null output, assigning it to output '{s}'", .{output.wlr_output.name});
wlr_layer_surface.output = output.wlr_output;
}
LayerSurface.create(wlr_layer_surface) catch {
wlr_layer_surface.resource.postNoMemory();
return;
};
}
fn handleNewXwaylandSurface(_: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void {
log.debug(
"new xwayland surface: title='{?s}', class='{?s}', override redirect={}",
.{ xwayland_surface.title, xwayland_surface.class, xwayland_surface.override_redirect },
);
if (xwayland_surface.override_redirect) {
_ = XwaylandOverrideRedirect.create(xwayland_surface) catch {
log.err("out of memory", .{});
return;
};
} else {
_ = XwaylandView.create(xwayland_surface) catch {
log.err("out of memory", .{});
return;
};
}
}
fn handleRequestActivate(
listener: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
event: *wlr.XdgActivationV1.event.RequestActivate,
) void {
const server: *Server = @fieldParentPtr("request_activate", listener);
const node_data = SceneNodeData.fromSurface(event.surface) orelse return;
switch (node_data.data) {
.view => |view| if (view.pending.focus == 0) {
view.pending.urgent = true;
server.root.applyPending();
},
else => |tag| {
log.info("ignoring xdg-activation-v1 activate request of {s} surface", .{@tagName(tag)});
},
}
}
fn handleRequestSetCursorShape(
_: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape),
event: *wlr.CursorShapeManagerV1.event.RequestSetShape,
) void {
const seat: *Seat = @ptrCast(@alignCast(event.seat_client.seat.data));
if (event.tablet_tool) |wp_tool| {
assert(event.device_type == .tablet_tool);
const tool = TabletTool.get(event.seat_client.seat, wp_tool.wlr_tool) catch return;
if (tool.allowSetCursor(event.seat_client, event.serial)) {
const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
tool.wlr_cursor.setXcursor(seat.cursor.xcursor_manager, name);
}
} else {
assert(event.device_type == .pointer);
const focused_client = event.seat_client.seat.pointer_state.focused_client;
// This can be sent by any client, so we check to make sure this one is
// actually has pointer focus first.
if (focused_client == event.seat_client) {
const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
seat.cursor.setImage(.{ .xcursor = name });
}
}
}
fn handleNewForeignToplevelCaptureRequest(
listener: *wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request),
request: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request,
) void {
const server: *Server = @fieldParentPtr("new_foreign_toplevel_capture_request", listener);
if (request.toplevel_handle.data) |opaque_view| {
const view: *View = @ptrCast(@alignCast(opaque_view));
const capture_source = view.image_capture_source orelse wlr.ExtImageCaptureSourceV1.createWithSceneNode(
&view.image_capture_scene.tree.node,
server.wl_server.getEventLoop(),
server.allocator,
server.renderer,
) catch {
log.err("failed to create ext image capture source", .{});
return;
};
_ = request.accept(capture_source);
}
}