Socket activation for `foot --server` #890

Manually merged
dnkl merged 5 commits from VannTen/foot:socket_activation into master 4 months ago
  1. 2
      CHANGELOG.md
  2. 2
      INSTALL.md
  3. 3
      README.md
  4. 15
      doc/foot.1.scd
  5. 13
      foot-server@.service.in
  6. 5
      foot-server@.socket
  7. 19
      meson.build
  8. 117
      server.c

2
CHANGELOG.md

@ -44,7 +44,7 @@
* OSC-22 - set xcursor pointer.
* Add "xterm" as fallback cursor where "text" is not available.
* `[key-bindings].scrollback-home|end` options.
* Socket activation for `foot --server` and accompanying systemd unit files
### Changed

2
INSTALL.md

@ -56,7 +56,6 @@ following **optional** dependencies:
* xdg-utils: URLs are by default launched with `xdg-open`.
* bash-completion: If you want completion for positional arguments.
### Building
In addition to the dev variant of the packages above, you need:
@ -68,6 +67,7 @@ In addition to the dev variant of the packages above, you need:
* scdoc (for man page generation, not needed if documentation is disabled)
* llvm (for PGO builds with Clang)
* [tllist](https://codeberg.org/dnkl/tllist) [^1]
* systemd (optional, foot will install systemd unit files if detected)
A note on compilers; in general, foot runs **much** faster when
compiled with gcc instead of clang. A profile-guided gcc build can be

3
README.md

@ -266,6 +266,9 @@ when starting your Wayland compositor (i.e. logging in to your
desktop), and then run `footclient` instead of `foot` whenever you
want to launch a new terminal.
Foot support socket activation, which means `foot --server` will only be
started the first time you'll run `footclient`. (systemd user units are
included, but it can work with other supervision suites).
## URLs

15
doc/foot.1.scd

@ -81,7 +81,7 @@ the foot command line
Initial working directory for the client application. Default:
_CWD of foot_.
*-s*,*--server*[=_PATH_]
*-s*,*--server*[=_PATH_|_FD_]
Run as a server. In this mode, a single foot instance hosts
multiple terminals (windows). Use *footclient*(1) to launch new
terminals.
@ -116,6 +116,19 @@ the foot command line
*--server-socket* option in *footclient*(1) and point it to your
custom socket path.
If the argument is a number, foot will interpret it as the file descriptor
of a socket provided by a supervision daemon (such as systemd or s6), and
use that socket as it's own.
Two systemd units (foot-server@.{service,socket}) are provided to use that
feature with systemd. They need to be instantiated with the value of
$WAYLAND_DISPLAY (multiples instances can co-exists).
Note that starting *foot --server* as a systemd service will use
the environment of the systemd user instance; thus, if you need specific
environment variables, you'll need to import them using *systemctl --user
import-environment* or use a drop-in for the foot-server service.
*-H*,*--hold*
Remain open after child process exits.

13
foot-server@.service.in

@ -0,0 +1,13 @@
[Service]
ExecStart=@bindir@/foot --server=0
Environment=WAYLAND_DISPLAY=%i
NonBlocking=true
StandardInput=socket
[Unit]
Requires=%N.socket
Description=Foot terminal server mode for WAYLAND_DISPLAY=%i
Documentation=man:foot(1)
[Install]
WantedBy=wayland-instance@.target

5
foot-server@.socket

@ -0,0 +1,5 @@
[Socket]
ListenStream=%t/foot-%i.sock
[Install]
WantedBy=wayland-instance@.target

19
meson.build

@ -245,6 +245,25 @@ install_data(
'foot.desktop', 'foot-server.desktop', 'footclient.desktop',
install_dir: join_paths(get_option('datadir'), 'applications'))
systemd = dependency('systemd', required: false)
if systemd.found()
configuration = configuration_data()
configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir')))
systemd_units_dir = systemd.get_pkgconfig_variable('systemduserunitdir')
configure_file(
configuration: configuration,
input: 'foot-server@.service.in',
output: '@BASENAME@',
install_dir: systemd_units_dir
)
install_data(
'foot-server@.socket',
install_dir: systemd_units_dir)
endif
scdoc = dependency('scdoc', native: true, required: get_option('docs'))
if scdoc.found()
install_data(

117
server.c

@ -1,6 +1,7 @@
#include "server.h"
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
@ -19,6 +20,7 @@
#include "client-protocol.h"
#include "shm.h"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xmalloc.h"
@ -425,44 +427,107 @@ err:
return ret;
}
static bool
prepare_socket(int fd)
{
int flags = fcntl(fd, F_GETFD);
if (flags < 0) {
LOG_ERRNO("failed to get file descriptors flag for passed socket");
return false;
}
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
LOG_ERRNO("failed to set FD_CLOEXEC for passed socket");
return false;
}
flags = fcntl(fd, F_GETFL);
if (flags < 0) {
LOG_ERRNO("failed to get file status flags for passed socket");
return false;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
LOG_ERRNO("failed to set non-blocking mode on passed socket");
return false;
}
int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE };
int const socket_options_values[] = { AF_UNIX, 1, SOCK_STREAM};
char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" };
xassert(ALEN(socket_options) == ALEN(socket_options_values));
xassert(ALEN(socket_options) == ALEN(socket_options_names));
int socket_option = 0;
socklen_t len;
for (size_t i = 0; i < ALEN(socket_options) ; i++) {
len = sizeof(socket_option);
if (getsockopt(fd, SOL_SOCKET, socket_options[i], &socket_option, &len) == -1 ||
len != sizeof(socket_option)) {
LOG_ERRNO("failed to read socket option from passed file descriptor");
return false;
}
if (socket_option != socket_options_values[i]) {
LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor",
socket_options_names[i]);
return false;
}
}
return true;
}
struct server *
server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
struct wayland *wayl)
{
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (fd == -1) {
LOG_ERRNO("failed to create UNIX socket");
return NULL;
}
int fd;
struct server *server = NULL;
const char *sock_path = conf->server_socket_path;
char *end;
switch (try_connect(sock_path)) {
case CONNECT_FAIL:
break;
errno = 0;
fd = strtol(sock_path, &end, 10);
if (*end == '\0' && *sock_path != '\0')
{
if (!prepare_socket(fd))
goto err;
LOG_DBG("we've been started by socket activation, using passed socket");
sock_path = NULL;
}
else {
LOG_DBG("no suitable pre-existing socket found, creating our own");
fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (fd == -1) {
LOG_ERRNO("failed to create UNIX socket");
return NULL;
}
case CONNECT_SUCCESS:
LOG_ERR("%s is already accepting connections; is 'foot --server' already running", sock_path);
/* FALLTHROUGH */
switch (try_connect(sock_path)) {
case CONNECT_FAIL:
break;
case CONNECT_ERR:
goto err;
}
case CONNECT_SUCCESS:
LOG_ERR("%s is already accepting connections; is 'foot --server' already running", sock_path);
/* FALLTHROUGH */
unlink(sock_path);
case CONNECT_ERR:
goto err;
}
struct sockaddr_un addr = {.sun_family = AF_UNIX};
strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1);
unlink(sock_path);
if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
LOG_ERRNO("%s: failed to bind", addr.sun_path);
goto err;
}
struct sockaddr_un addr = {.sun_family = AF_UNIX};
strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1);
if (listen(fd, 0) < 0) {
LOG_ERRNO("%s: failed to listen", addr.sun_path);
goto err;
if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
LOG_ERRNO("%s: failed to bind", addr.sun_path);
goto err;
}
if (listen(fd, 0) < 0) {
LOG_ERRNO("%s: failed to listen", addr.sun_path);
goto err;
}
}
server = malloc(sizeof(*server));
@ -487,7 +552,7 @@ server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
if (!fdm_add(fdm, fd, EPOLLIN, &fdm_server, server))
goto err;
LOG_INFO("accepting connections on %s", sock_path);
LOG_INFO("accepting connections on %s", sock_path != NULL ? sock_path : "socket provided through socket activation");
return server;

Loading…
Cancel
Save