1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
package SocketActivation;
# vim:ts=4:sw=4:expandtab
use strict;
use warnings;
use IO::Socket::UNIX; # core
use Cwd qw(abs_path); # core
use POSIX qw(:fcntl_h); # core
use AnyEvent::Handle; # not core
use AnyEvent::Util; # not core
use Exporter 'import';
use v5.10;
our @EXPORT = qw(activate_i3);
#
# Starts i3 using socket activation. Creates a listening socket (with bind +
# listen) which is then passed to i3, who in turn calls accept and handles the
# requests.
#
# Since the kernel buffers the connect, the parent process can connect to the
# socket immediately after forking. It then sends a request and waits until it
# gets an answer. Obviously, i3 has to be initialized to actually answer the
# request.
#
# This way, we can wait *precisely* the amount of time which i3 waits to get
# ready, which is a *HUGE* speed gain (and a lot more robust) in comparison to
# using sleep() with a fixed amount of time.
#
# unix_socket_path: Location of the socket to use for the activation
# display: X11 $ENV{DISPLAY}
# configfile: path to the configuration file to use
# logpath: path to the logfile to which i3 will append
# cv: an AnyEvent->condvar which will be triggered once i3 is ready
#
sub activate_i3 {
my %args = @_;
# remove the old unix socket
unlink($args{unix_socket_path});
my $socket = IO::Socket::UNIX->new(
Listen => 1,
Local => $args{unix_socket_path},
);
my $pid = fork;
if (!defined($pid)) {
die "could not fork()";
}
if ($pid == 0) {
# Start a process group so that in the parent, we can kill the entire
# process group and immediately kill i3bar and any other child
# processes.
setpgrp;
$ENV{LISTEN_PID} = $$;
$ENV{LISTEN_FDS} = 1;
delete $ENV{DESKTOP_STARTUP_ID};
delete $ENV{I3SOCK};
# $SHELL could be set to fish, which will horribly break running shell
# commands via i3’s exec feature. This happened e.g. when having
# “set-option -g default-shell "/usr/bin/fish"” in ~/.tmux.conf
delete $ENV{SHELL};
unless ($args{dont_create_temp_dir}) {
$ENV{XDG_RUNTIME_DIR} = '/tmp/i3-testsuite/';
mkdir $ENV{XDG_RUNTIME_DIR};
}
$ENV{DISPLAY} = $args{display};
# We are about to exec, but we did not modify $^F to include $socket
# when creating the socket (because the file descriptor could have a
# number != 3 which would lead to i3 leaking a file descriptor). This
# caused Perl to set the FD_CLOEXEC flag, which would close $socket on
# exec(), effectively *NOT* passing $socket to the new process.
# Therefore, we explicitly clear FD_CLOEXEC (the only flag right now)
# by setting the flags to 0.
POSIX::fcntl($socket, F_SETFD, 0) or die "Could not clear fd flags: $!";
# If the socket does not use file descriptor 3 by chance already, we
# close fd 3 and dup2() the socket to 3.
if (fileno($socket) != 3) {
POSIX::close(3);
POSIX::dup2(fileno($socket), 3);
POSIX::close(fileno($socket));
}
# Make sure no file descriptors are open. Strangely, I got an open file
# descriptor pointing to AnyEvent/Impl/EV.pm when testing.
AnyEvent::Util::close_all_fds_except(0, 1, 2, 3);
# Construct the command to launch i3. Use maximum debug level, disable
# the interactive signalhandler to make it crash immediately instead.
# Also disable logging to SHM since we redirect the logs anyways.
# Force Xinerama because we use Xdmx for multi-monitor tests.
my $i3cmd = q|i3 --shmlog-size=0 --disable-signalhandler|;
if (!defined($args{inject_randr15})) {
$i3cmd .= q| --force-xinerama|;
}
if (!$args{validate_config}) {
# We only set logging if i3 is actually started, but not if we only
# validate the config file. This is to keep logging to a minimum as
# such a test will likely want to inspect the log file.
$i3cmd .= q| -V -d all|;
}
# For convenience:
my $outdir = $args{outdir};
my $test = $args{testname};
if ($args{restart}) {
$i3cmd .= ' -L ' . abs_path('restart-state.golden');
}
if ($args{validate_config}) {
$i3cmd .= ' -C';
}
if ($args{valgrind}) {
$i3cmd =
qq|valgrind --log-file="$outdir/valgrind-for-$test.log" | .
qq|--suppressions="./valgrind.supp" | .
qq|--leak-check=full --track-origins=yes --num-callers=20 | .
qq|--tool=memcheck -- $i3cmd|;
}
my $logfile = "$outdir/i3-log-for-$test";
# Append to $logfile instead of overwriting because i3 might be
# run multiple times in one testcase.
my $cmd = "exec $i3cmd -c $args{configfile} >>$logfile 2>&1";
if ($args{strace}) {
my $out = "$outdir/strace-for-$test.log";
# We overwrite LISTEN_PID with the correct process ID to make
# socket activation work (LISTEN_PID has to match getpid(),
# otherwise the LISTEN_FDS will be treated as a left-over).
$cmd = qq|strace -fvy -s2048 -o "$out" -- | .
'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"';
}
if ($args{xtrace}) {
my $out = "$outdir/xtrace-for-$test.log";
# See comment in $args{strace} branch.
$cmd = qq|xtrace -n -o "$out" -- | .
'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"';
}
if ($args{inject_randr15}) {
# See comment in $args{strace} branch.
$cmd = 'test.inject_randr15 --getmonitors_reply="' .
$args{inject_randr15} . '" ' .
($args{inject_randr15_outputinfo}
? '--getoutputinfo_reply="' .
$args{inject_randr15_outputinfo} . '" '
: '') .
'-- ' .
'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"';
}
# We need to use the shell due to using output redirections.
exec '/bin/sh', '-c', $cmd;
# if we are still here, i3 could not be found or exec failed. bail out.
exit 1;
}
# close the socket, the child process should be the only one which keeps a file
# descriptor on the listening socket.
$socket->close;
if ($args{validate_config}) {
$args{cv}->send(1);
return $pid;
}
# We now connect (will succeed immediately) and send a request afterwards.
# As soon as the reply is there, i3 is considered ready.
my $cl = IO::Socket::UNIX->new(Peer => $args{unix_socket_path});
my $hdl;
$hdl = AnyEvent::Handle->new(
fh => $cl,
on_error => sub {
$hdl->destroy;
$args{cv}->send(0);
});
# send a get_tree message without payload
$hdl->push_write('i3-ipc' . pack("LL", 0, 4));
# wait for the reply
$hdl->push_read(chunk => 1, => sub {
my ($h, $line) = @_;
$args{cv}->send(1);
undef $hdl;
});
return $pid;
}
1
|