466 lines
18 KiB
Zig
466 lines
18 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 wlr = @import("wlroots");
|
|
const wl = @import("wayland").server.wl;
|
|
|
|
const c = @import("c.zig");
|
|
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 XdgDecoration = @import("XdgDecoration.zig");
|
|
const XdgToplevel = @import("XdgToplevel.zig");
|
|
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
|
|
const XwaylandView = @import("XwaylandView.zig");
|
|
|
|
const log = std.log.scoped(.server);
|
|
|
|
wl_server: *wl.Server,
|
|
|
|
sigint_source: *wl.EventSource,
|
|
sigterm_source: *wl.EventSource,
|
|
|
|
backend: *wlr.Backend,
|
|
session: ?*wlr.Session,
|
|
|
|
renderer: *wlr.Renderer,
|
|
allocator: *wlr.Allocator,
|
|
|
|
security_context_manager: *wlr.SecurityContextManagerV1,
|
|
|
|
shm: *wlr.Shm,
|
|
drm: ?*wlr.Drm = null,
|
|
linux_dmabuf: ?*wlr.LinuxDmabufV1 = null,
|
|
single_pixel_buffer_manager: *wlr.SinglePixelBufferManagerV1,
|
|
|
|
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,
|
|
|
|
foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
|
|
|
|
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),
|
|
|
|
new_xdg_surface: wl.Listener(*wlr.XdgSurface) =
|
|
wl.Listener(*wlr.XdgSurface).init(handleNewXdgSurface),
|
|
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),
|
|
|
|
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();
|
|
|
|
var session: ?*wlr.Session = undefined;
|
|
const backend = try wlr.Backend.autocreate(wl_server, &session);
|
|
const renderer = try wlr.Renderer.autocreate(backend);
|
|
|
|
const compositor = try wlr.Compositor.create(wl_server, 6, renderer);
|
|
|
|
const loop = wl_server.getEventLoop();
|
|
server.* = .{
|
|
.wl_server = wl_server,
|
|
.sigint_source = try loop.addSignal(*wl.Server, std.os.SIG.INT, terminate, wl_server),
|
|
.sigterm_source = try loop.addSignal(*wl.Server, std.os.SIG.TERM, terminate, wl_server),
|
|
|
|
.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, 1, renderer),
|
|
.single_pixel_buffer_manager = try wlr.SinglePixelBufferManagerV1.create(wl_server),
|
|
|
|
.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, 1),
|
|
|
|
.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),
|
|
|
|
.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.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.getDmabufFormats() != null and renderer.getDrmFd() >= 0) {
|
|
// wl_drm is a legacy interface and all clients should switch to linux_dmabuf.
|
|
// However, enough widely used clients still rely on wl_drm that the pragmatic option
|
|
// is to keep it around for the near future.
|
|
// TODO remove wl_drm support
|
|
server.drm = try wlr.Drm.create(wl_server, renderer);
|
|
|
|
server.linux_dmabuf = try wlr.LinuxDmabufV1.createWithRenderer(wl_server, 4, renderer);
|
|
}
|
|
|
|
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.xdg_shell.events.new_surface.add(&server.new_xdg_surface);
|
|
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);
|
|
|
|
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.new_xdg_surface.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();
|
|
|
|
if (build_options.xwayland) {
|
|
if (server.xwayland) |xwayland| {
|
|
server.new_xwayland_surface.link.remove();
|
|
xwayland.destroy();
|
|
}
|
|
}
|
|
|
|
server.wl_server.destroyClients();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
fn hackGlobal(ptr: *anyopaque) *wl.Global {
|
|
// TODO(wlroots) MR that eliminates the need for this hack:
|
|
// https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4612
|
|
if (wlr.version.major != 0 or wlr.version.minor != 17) @compileError("FIXME");
|
|
|
|
return @as(*extern struct { global: *wl.Global }, @alignCast(@ptrCast(ptr))).global;
|
|
}
|
|
|
|
/// Returns true if the global is allowlisted for security contexts
|
|
fn allowlist(server: *Server, global: *const wl.Global) bool {
|
|
if (server.drm) |drm| if (global == drm.global) return true;
|
|
if (server.linux_dmabuf) |linux_dmabuf| if (global == linux_dmabuf.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.
|
|
//
|
|
// 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.getInterface() == wl.Output.getInterface() or
|
|
global.getInterface() == wl.Seat.getInterface() or
|
|
global == hackGlobal(server.shm) or
|
|
global == hackGlobal(server.single_pixel_buffer_manager) 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;
|
|
}
|
|
|
|
/// 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.screencopy_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 == hackGlobal(server.input_manager.idle_notifier) 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 handleNewXdgSurface(_: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void {
|
|
if (xdg_surface.role == .popup) {
|
|
log.debug("new xdg_popup", .{});
|
|
return;
|
|
}
|
|
|
|
log.debug("new xdg_toplevel", .{});
|
|
|
|
XdgToplevel.create(xdg_surface.role_data.toplevel.?) catch {
|
|
log.err("out of memory", .{});
|
|
xdg_surface.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 = @fieldParentPtr(Server, "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 = @fieldParentPtr(Server, "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 {
|
|
// Ignore requests to set a tablet tool's cursor shape for now
|
|
// TODO(wlroots): https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3821
|
|
if (event.device_type == .tablet_tool) return;
|
|
|
|
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 seat: *Seat = @ptrFromInt(event.seat_client.seat.data);
|
|
const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
|
|
seat.cursor.setXcursor(name);
|
|
}
|
|
}
|