Skip to content

Commit ce21de8

Browse files
committed
Implement the tick event
This makes our tests less flaky, shorter, and more readable. fixes i3#2988
1 parent 14c8cf8 commit ce21de8

29 files changed

+463
-740
lines changed

AnyEvent-I3/lib/AnyEvent/I3.pm

+15-1
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ use constant TYPE_GET_BAR_CONFIG => 6;
9999
use constant TYPE_GET_VERSION => 7;
100100
use constant TYPE_GET_BINDING_MODES => 8;
101101
use constant TYPE_GET_CONFIG => 9;
102+
use constant TYPE_SEND_TICK => 10;
102103

103104
our %EXPORT_TAGS = ( 'all' => [
104105
qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
105106
TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
106-
TYPE_GET_BINDING_MODES TYPE_GET_CONFIG)
107+
TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK)
107108
] );
108109

109110
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
@@ -120,6 +121,7 @@ my %events = (
120121
barconfig_update => ($event_mask | 4),
121122
binding => ($event_mask | 5),
122123
shutdown => ($event_mask | 6),
124+
tick => ($event_mask | 7),
123125
_error => 0xFFFFFFFF,
124126
);
125127

@@ -519,6 +521,18 @@ sub get_config {
519521
$self->message(TYPE_GET_CONFIG);
520522
}
521523

524+
=head2 send_tick
525+
526+
Sends a tick event. Requires i3 >= 4.15
527+
528+
=cut
529+
sub send_tick {
530+
my ($self, $payload) = @_;
531+
532+
$self->_ensure_connection;
533+
534+
$self->message(TYPE_SEND_TICK, $payload);
535+
}
522536

523537
=head2 command($content)
524538

docs/ipc

+41
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ to do that).
6464
| 7 | +GET_VERSION+ | <<_version_reply,VERSION>> | Gets the i3 version.
6565
| 8 | +GET_BINDING_MODES+ | <<_binding_modes_reply,BINDING_MODES>> | Gets the names of all currently configured binding modes.
6666
| 9 | +GET_CONFIG+ | <<_config_reply,CONFIG>> | Returns the last loaded i3 config.
67+
| 10 | +SEND_TICK+ | <<_tick_reply,TICK>> | Sends a tick event with the specified payload.
6768
|======================================================
6869

6970
So, a typical message could look like this:
@@ -126,6 +127,8 @@ BINDING_MODES (8)::
126127
Reply to the GET_BINDING_MODES message.
127128
GET_CONFIG (9)::
128129
Reply to the GET_CONFIG message.
130+
TICK (10)::
131+
Reply to the SEND_TICK message.
129132

130133
[[_command_reply]]
131134
=== COMMAND reply
@@ -637,6 +640,19 @@ which is a string containing the config file as loaded by i3 most recently.
637640
{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" }
638641
-------------------
639642

643+
[[_tick_reply]]
644+
=== TICK reply
645+
646+
The reply is a map containing the "success" member. After the reply was
647+
received, the tick event has been written to all IPC connections which subscribe
648+
to tick events. UNIX sockets are usually buffered, but you can be certain that
649+
once you receive the tick event you just triggered, you must have received all
650+
events generated prior to the +SEND_TICK+ message (happened-before relation).
651+
652+
*Example:*
653+
-------------------
654+
{ "success": true }
655+
-------------------
640656

641657
== Events
642658

@@ -694,6 +710,10 @@ binding (5)::
694710
mouse
695711
shutdown (6)::
696712
Sent when the ipc shuts down because of a restart or exit by user command
713+
tick (7)::
714+
Sent when the ipc client subscribes to the tick event (with +"first":
715+
true+) or when any ipc client sends a SEND_TICK message (with +"first":
716+
false+).
697717

698718
*Example:*
699719
--------------------------------------------------------------------
@@ -866,6 +886,27 @@ because of a user action such as a +restart+ or +exit+ command. The +change
866886
}
867887
---------------------------
868888

889+
=== tick event
890+
891+
This event is triggered by a subscription to tick events or by a +SEND_TICK+
892+
message.
893+
894+
*Example (upon subscription):*
895+
--------------------------------------------------------------------------------
896+
{
897+
"first": true,
898+
"payload": ""
899+
}
900+
--------------------------------------------------------------------------------
901+
902+
*Example (upon +SEND_TICK+ with a payload of +arbitrary string+):*
903+
--------------------------------------------------------------------------------
904+
{
905+
"first": false,
906+
"payload": "arbitrary string"
907+
}
908+
--------------------------------------------------------------------------------
909+
869910
== See also (existing libraries)
870911

871912
[[libraries]]

i3-msg/main.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,11 @@ int main(int argc, char *argv[]) {
207207
message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION;
208208
} else if (strcasecmp(optarg, "get_config") == 0) {
209209
message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG;
210+
} else if (strcasecmp(optarg, "send_tick") == 0) {
211+
message_type = I3_IPC_MESSAGE_TYPE_SEND_TICK;
210212
} else {
211213
printf("Unknown message type\n");
212-
printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n");
214+
printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config, send_tick\n");
213215
exit(EXIT_FAILURE);
214216
}
215217
} else if (o == 'q') {

include/i3/ipc.h

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ typedef struct i3_ipc_header {
6060
/** Request the raw last loaded i3 config. */
6161
#define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9
6262

63+
/** Send a tick event to all subscribers. */
64+
#define I3_IPC_MESSAGE_TYPE_SEND_TICK 10
65+
6366
/*
6467
* Messages from i3 to clients
6568
*
@@ -74,6 +77,7 @@ typedef struct i3_ipc_header {
7477
#define I3_IPC_REPLY_TYPE_VERSION 7
7578
#define I3_IPC_REPLY_TYPE_BINDING_MODES 8
7679
#define I3_IPC_REPLY_TYPE_CONFIG 9
80+
#define I3_IPC_REPLY_TYPE_TICK 10
7781

7882
/*
7983
* Events from i3 to clients. Events have the first bit set high.
@@ -101,3 +105,6 @@ typedef struct i3_ipc_header {
101105

102106
/** The shutdown event will be triggered when the ipc shuts down */
103107
#define I3_IPC_EVENT_SHUTDOWN (I3_IPC_EVENT_MASK | 6)
108+
109+
/** The tick event will be sent upon a tick IPC message */
110+
#define I3_IPC_EVENT_TICK (I3_IPC_EVENT_MASK | 7)

include/ipc.h

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ typedef struct ipc_client {
3131
int num_events;
3232
char **events;
3333

34+
/* For clients which subscribe to the tick event: whether the first tick
35+
* event has been sent by i3. */
36+
bool first_tick_sent;
37+
3438
TAILQ_ENTRY(ipc_client)
3539
clients;
3640
} ipc_client;

src/ipc.c

+49-2
Original file line numberDiff line numberDiff line change
@@ -1046,8 +1046,9 @@ static int add_subscription(void *extra, const unsigned char *s,
10461046
memcpy(client->events[event], s, len);
10471047

10481048
DLOG("client is now subscribed to:\n");
1049-
for (int i = 0; i < client->num_events; i++)
1049+
for (int i = 0; i < client->num_events; i++) {
10501050
DLOG("event %s\n", client->events[i]);
1051+
}
10511052
DLOG("(done)\n");
10521053

10531054
return 1;
@@ -1099,6 +1100,25 @@ IPC_HANDLER(subscribe) {
10991100
yajl_free(p);
11001101
const char *reply = "{\"success\":true}";
11011102
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply);
1103+
1104+
if (client->first_tick_sent) {
1105+
return;
1106+
}
1107+
1108+
bool is_tick = false;
1109+
for (int i = 0; i < client->num_events; i++) {
1110+
if (strcmp(client->events[i], "tick") == 0) {
1111+
is_tick = true;
1112+
break;
1113+
}
1114+
}
1115+
if (!is_tick) {
1116+
return;
1117+
}
1118+
1119+
client->first_tick_sent = true;
1120+
const char *payload = "{\"first\":true,\"payload\":\"\"}";
1121+
ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload);
11021122
}
11031123

11041124
/*
@@ -1122,9 +1142,35 @@ IPC_HANDLER(get_config) {
11221142
y(free);
11231143
}
11241144

1145+
/*
1146+
* Sends the tick event from the message payload to subscribers. Establishes a
1147+
* synchronization point in event-related tests.
1148+
*/
1149+
IPC_HANDLER(send_tick) {
1150+
yajl_gen gen = ygenalloc();
1151+
1152+
y(map_open);
1153+
1154+
ystr("payload");
1155+
yajl_gen_string(gen, (unsigned char *)message, message_size);
1156+
1157+
y(map_close);
1158+
1159+
const unsigned char *payload;
1160+
ylength length;
1161+
y(get_buf, &payload, &length);
1162+
1163+
ipc_send_event("tick", I3_IPC_EVENT_TICK, (const char *)payload);
1164+
y(free);
1165+
1166+
const char *reply = "{\"success\":true}";
1167+
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply);
1168+
DLOG("Sent tick event\n");
1169+
}
1170+
11251171
/* The index of each callback function corresponds to the numeric
11261172
* value of the message type (see include/i3/ipc.h) */
1127-
handler_t handlers[10] = {
1173+
handler_t handlers[11] = {
11281174
handle_run_command,
11291175
handle_get_workspaces,
11301176
handle_subscribe,
@@ -1135,6 +1181,7 @@ handler_t handlers[10] = {
11351181
handle_get_version,
11361182
handle_get_binding_modes,
11371183
handle_get_config,
1184+
handle_send_tick,
11381185
};
11391186

11401187
/*

testcases/lib/i3test.pm.in

+82
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ our @EXPORT = qw(
4747
wait_for_unmap
4848
$x
4949
kill_all_windows
50+
events_for
51+
listen_for_binding
5052
);
5153

5254
=head1 NAME
@@ -900,6 +902,86 @@ sub kill_all_windows {
900902
cmd '[title=".*"] kill';
901903
}
902904

905+
=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
906+
907+
Helper function which returns an array containing all events of type $rettype
908+
which were generated by i3 while $subscribecb was running.
909+
910+
Set $eventcbs to subscribe to multiple event types and/or perform your own event
911+
aggregation.
912+
913+
=cut
914+
sub events_for {
915+
my ($subscribecb, $rettype, $eventcbs) = @_;
916+
917+
my @events;
918+
$eventcbs //= {};
919+
if (defined($rettype)) {
920+
$eventcbs->{$rettype} = sub { push @events, shift };
921+
}
922+
my $subscribed = AnyEvent->condvar;
923+
my $flushed = AnyEvent->condvar;
924+
$eventcbs->{tick} = sub {
925+
my ($event) = @_;
926+
if ($event->{first}) {
927+
$subscribed->send($event);
928+
} else {
929+
$flushed->send($event);
930+
}
931+
};
932+
my $i3 = i3(get_socket_path(0));
933+
$i3->connect->recv;
934+
$i3->subscribe($eventcbs)->recv;
935+
$subscribed->recv;
936+
# Subscription established, run the callback.
937+
$subscribecb->();
938+
# Now generate a tick event, which we know we’ll receive (and at which point
939+
# all other events have been received).
940+
my $nonce = int(rand(255)) + 1;
941+
$i3->send_tick($nonce);
942+
943+
my $tick = $flushed->recv;
944+
$tester->is_eq($tick->{payload}, $nonce, 'tick nonce received');
945+
return @events;
946+
}
947+
948+
=head2 listen_for_binding($cb)
949+
950+
Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST
951+
triggers an i3 key binding or not. Expects key bindings to be configured in the
952+
form “bindsym <binding> nop <binding>”, e.g. “bindsym Mod4+Return nop
953+
Mod4+Return”.
954+
955+
is(listen_for_binding(
956+
sub {
957+
xtest_key_press(133); # Super_L
958+
xtest_key_press(36); # Return
959+
xtest_key_release(36); # Return
960+
xtest_key_release(133); # Super_L
961+
xtest_sync_with_i3;
962+
},
963+
),
964+
'Mod4+Return',
965+
'triggered the "Mod4+Return" keybinding');
966+
967+
=cut
968+
969+
sub listen_for_binding {
970+
my ($cb) = @_;
971+
my $triggered = AnyEvent->condvar;
972+
my @events = events_for(
973+
$cb,
974+
'binding');
975+
976+
$tester->is_eq(scalar @events, 1, 'Received precisely one event');
977+
$tester->is_eq($events[0]->{change}, 'run', 'change is "run"');
978+
# We look at the command (which is “nop <binding>”) because that is easier
979+
# than re-assembling the string representation of $event->{binding}.
980+
my $command = $events[0]->{binding}->{command};
981+
$command =~ s/^nop //g;
982+
return $command;
983+
}
984+
903985
=head1 AUTHOR
904986

905987
Michael Stapelberg <[email protected]>

0 commit comments

Comments
 (0)