Skip to content

Commit 969b502

Browse files
committed
Add grace period feature
This includes: * A --grace flag to specify how long to wait before requiring a password * A --pointer-hysteresis flag to prevent minor mouse wiggles from unlocking the screen * A helper process that ensures the screen locks before the system sleeps (which adds a dependency on logind, for sleep inhibition) * A SIGINT2 handler to end the grace period
1 parent c112872 commit 969b502

File tree

9 files changed

+569
-4
lines changed

9 files changed

+569
-4
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,45 @@ restart swaylock-plugin; or by switching to a different virtual terminal, runnin
4343
4444
See the man page, [`swaylock-plugin(1)`](swaylock.1.scd), for instructions on using swaylock-plugin.
4545
46+
## Grace period
47+
48+
`swaylock-plugin` adds a grace period feature; unlike the original `swaylock`, it
49+
is not practical to emulate one using a separate program (like `chayang`) because
50+
any animated backgrounds would be interrupted. With the `--grace` flag, it is
51+
possible to unlock the screen without a password for the first few seconds after
52+
the screen locker starts with either a key press or significant mouse motion.
53+
54+
This feature requires logind (systemd or elogind) support to automatically end the
55+
grace period just before the computer goes to sleep. The grace period also ends on
56+
receipt of the signal SIGUSR2.
57+
58+
### Example
59+
60+
Sway can be made to lock the screen with a grace period and the custom wallpaper
61+
program specified in the script `lock-bg-command.sh` with the following configuration:
62+
63+
```
64+
exec swayidle \
65+
timeout 300 'swaylock-plugin --grace 30sec --pointer-hysteresis 25.0 --command-each lock-bg-command.sh' \
66+
timeout 600 'swaymsg "output * dpms off"' \
67+
resume 'swaymsg "output * dpms on"' \
68+
before-sleep 'swaylock-plugin --command-each lock-bg-command.sh'
69+
bindsym --locked Ctrl+Alt+L exec \
70+
'killall -SIGUSR2 swaylock-plugin; \
71+
swaylock-plugin --command-each lock-bg-command.sh'
72+
```
73+
74+
This will, after 5 minutes of inactivity, start `swaylock-plugin`; for the next
75+
30 seconds, one can easily unlock the screen by pressing any key or moving the
76+
mouse more than 25 pixels in a one second period; afterwards, authentication
77+
will be required. When the computer goes to sleep, the screen will lock for
78+
certain. (If `swaylock-plugin` was running and in the grace period, the grace
79+
period will end; in case `swaylock-plugin` was not running, a new instance will
80+
be started without a grace period, that locks the screen if it was not already
81+
locked.) One can also immediately lock the screen with a keybinding (or use the
82+
keybinding to restart the lock screen, if it crashed.) Any screens will be turned
83+
off after 10 minutes of inactivity.
84+
4685
## Installation
4786
4887
Install dependencies:
@@ -54,6 +93,7 @@ Install dependencies:
5493
* cairo
5594
* gdk-pixbuf2
5695
* pam (optional)
96+
* systemd or elogind (optional)
5797
* [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (optional: man pages) \*
5898
* git \*
5999
* swaybg

include/seat.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <xkbcommon/xkbcommon.h>
44
#include <stdint.h>
55
#include <stdbool.h>
6+
#include <wayland-util.h>
7+
#include <time.h>
68

79
struct loop;
810
struct loop_timer;
@@ -24,6 +26,10 @@ struct swaylock_seat {
2426
uint32_t repeat_sym;
2527
uint32_t repeat_codepoint;
2628
struct loop_timer *repeat_timer;
29+
/* mouse tracking to check for deliberate mouse motion */
30+
int64_t last_interval;
31+
wl_fixed_t interval_start_x, interval_start_y;
32+
wl_fixed_t last_mouse_x, last_mouse_y;
2733
};
2834

2935
extern const struct wl_seat_listener seat_listener;

include/swaylock.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ struct swaylock_args {
7676
bool indicator_idle_visible;
7777
char *plugin_command;
7878
bool plugin_per_output;
79+
/* negative values = no grace; unit: seconds */
80+
float grace_time;
81+
/* max number of pixels/sec mouse motion which will be ignored */
82+
float grace_pointer_hysteresis;
7983
};
8084

8185
struct swaylock_password {
@@ -303,6 +307,8 @@ struct swaylock_state {
303307
struct forward_state forward;
304308
struct swaylock_bg_server server;
305309
bool start_clientless_mode;
310+
struct loop_timer *grace_timer; // timer for grace period to end
311+
int sleep_comm_r, sleep_comm_w;
306312

307313
// for nested server, output was destroyed
308314
struct wl_list stale_wl_output_resources;

main.c

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,11 +674,16 @@ static const struct wl_registry_listener registry_listener = {
674674
};
675675

676676
static int sigusr_fds[2] = {-1, -1};
677+
static int sigusr2_fds[2] = {-1, -1};
677678

678679
void do_sigusr(int sig) {
679680
(void)write(sigusr_fds[1], "1", 1);
680681
}
681682

683+
void do_sigusr2(int sig) {
684+
(void)write(sigusr2_fds[1], "1", 1);
685+
}
686+
682687
static cairo_surface_t *select_image(struct swaylock_state *state,
683688
struct swaylock_surface *surface) {
684689
struct swaylock_image *image;
@@ -854,6 +859,8 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state,
854859
LO_TEXT_CAPS_LOCK_COLOR,
855860
LO_TEXT_VER_COLOR,
856861
LO_TEXT_WRONG_COLOR,
862+
LO_PLUGIN_GRACE,
863+
LO_PLUGIN_POINTER_HYSTERESIS,
857864
LO_PLUGIN_COMMAND,
858865
LO_PLUGIN_COMMAND_EACH,
859866
};
@@ -913,6 +920,8 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state,
913920
{"text-caps-lock-color", required_argument, NULL, LO_TEXT_CAPS_LOCK_COLOR},
914921
{"text-ver-color", required_argument, NULL, LO_TEXT_VER_COLOR},
915922
{"text-wrong-color", required_argument, NULL, LO_TEXT_WRONG_COLOR},
923+
{"grace", required_argument, NULL, LO_PLUGIN_GRACE},
924+
{"pointer-hysteresis", required_argument, NULL, LO_PLUGIN_POINTER_HYSTERESIS},
916925
{"command", required_argument, NULL, LO_PLUGIN_COMMAND},
917926
{"command-each", required_argument, NULL, LO_PLUGIN_COMMAND_EACH},
918927
{0, 0, 0, 0}
@@ -1037,6 +1046,10 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state,
10371046
"Sets the color of the text when verifying.\n"
10381047
" --text-wrong-color <color> "
10391048
"Sets the color of the text when invalid.\n"
1049+
" --grace <seconds> "
1050+
"Allow unlocking without a password before <seconds> elapse\n"
1051+
" --pointer-hysteresis <distance> "
1052+
"If --grace used, minimum mouse motion needed to auto-unlock\n"
10401053
" --command <cmd> "
10411054
"Indicates which program to run to draw backgrounds.\n"
10421055
" --command-each <cmd> "
@@ -1322,6 +1335,38 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state,
13221335
state->args.colors.text.wrong = parse_color(optarg);
13231336
}
13241337
break;
1338+
case LO_PLUGIN_GRACE:
1339+
if (state) {
1340+
char *end = NULL;
1341+
float value = strtof(optarg, &end);
1342+
float unit = 1.0;
1343+
if (*end == 0 || strcmp(end, "s") == 0 || strcmp(end, "sec") == 0) {
1344+
unit = 1.0;
1345+
} else if (strcmp(end, "m") == 0 || strcmp(end, "min") == 0) {
1346+
unit = 60.0;
1347+
} else if (strcmp(end, "h") == 0 || strcmp(end, "hr") == 0) {
1348+
unit = 3600.0;
1349+
} else {
1350+
swaylock_log(LOG_ERROR,
1351+
"'%s' is not a valid grace time specification. Valid examples: 11.5, 100s, 30min, 2hr",
1352+
optarg);
1353+
break;
1354+
}
1355+
state->args.grace_time = value * unit;
1356+
}
1357+
break;
1358+
case LO_PLUGIN_POINTER_HYSTERESIS:
1359+
if (state) {
1360+
char *end = NULL;
1361+
float value = strtof(optarg, &end);
1362+
if (*end == '\0') {
1363+
state->args.grace_pointer_hysteresis = value;
1364+
} else {
1365+
swaylock_log(LOG_ERROR,
1366+
"Invalid value for pointer hysteresis: '%s' is not a real number", optarg);
1367+
}
1368+
}
1369+
break;
13251370
case LO_PLUGIN_COMMAND:
13261371
if (state) {
13271372
free(state->args.plugin_command);
@@ -1852,6 +1897,19 @@ static void output_redraw_timeout(void *data) {
18521897
client_timeout(surface->client ? surface->client : surface->state->server.main_client);
18531898
}
18541899

1900+
static void grace_timeout(void *data) {
1901+
struct swaylock_state *state = data;
1902+
// The event loop frees the timer object; setting grace_timer to null marks
1903+
// the grace period as over
1904+
swaylock_log(LOG_DEBUG, "Grace period ended");
1905+
state->grace_timer = NULL;
1906+
loop_remove_fd(state->eventloop, state->sleep_comm_r);
1907+
close(state->sleep_comm_r);
1908+
close(state->sleep_comm_w);
1909+
state->sleep_comm_r = -1;
1910+
state->sleep_comm_w = -1;
1911+
}
1912+
18551913
uint32_t posix_spawn_setsid_flag(void);
18561914
static bool spawn_command(struct swaylock_state *state, int sock_child,
18571915
int sock_local, const char *output_name, const char *output_desc) {
@@ -2103,6 +2161,71 @@ static void term_in(int fd, short mask, void *data) {
21032161
state.run_display = false;
21042162
}
21052163

2164+
static void lock_in(int fd, short mask, void *data) {
2165+
/* On receipt of SIGUSR2, end the grace period */
2166+
if (state.grace_timer) {
2167+
/* Timer will automatically be freed later */
2168+
swaylock_log(LOG_DEBUG, "Received SIGUSR2, ending unlock grace period");
2169+
loop_remove_timer(state.eventloop, state.grace_timer);
2170+
state.grace_timer = NULL;
2171+
loop_remove_fd(state.eventloop, state.sleep_comm_r);
2172+
close(state.sleep_comm_r);
2173+
close(state.sleep_comm_w);
2174+
state.sleep_comm_r = -1;
2175+
state.sleep_comm_w = -1;
2176+
}
2177+
}
2178+
2179+
static void sleep_in(int fd, short mask, void *data) {
2180+
struct swaylock_state *state = data;
2181+
if (state->grace_timer) {
2182+
swaylock_log(LOG_DEBUG, "Received sleep notification, ending unlock grace period");
2183+
loop_remove_timer(state->eventloop, state->grace_timer);
2184+
state->grace_timer = NULL;
2185+
close(state->sleep_comm_r);
2186+
close(state->sleep_comm_w);
2187+
state->sleep_comm_r = -1;
2188+
state->sleep_comm_w = -1;
2189+
}
2190+
loop_remove_fd(state->eventloop, state->sleep_comm_r);
2191+
}
2192+
2193+
static int start_sleep_watcher(int *sleep_comm_r, int* sleep_comm_w) {
2194+
int comm_r[2], comm_w[2];
2195+
if (pipe(comm_r) != 0 || pipe(comm_w) != 0) {
2196+
swaylock_log(LOG_ERROR, "Failed to create communication pipes");
2197+
return -1;
2198+
}
2199+
2200+
if (!set_cloexec(comm_r[0]) || !set_cloexec(comm_w[1])) {
2201+
swaylock_log(LOG_ERROR, "Failed to make pipes close-on-exec");
2202+
close(comm_r[0]);
2203+
close(comm_r[1]);
2204+
close(comm_w[0]);
2205+
close(comm_w[1]);
2206+
return -1;
2207+
}
2208+
2209+
pid_t pid;
2210+
char proc_r_str[20], proc_w_str[20];
2211+
snprintf(proc_r_str, sizeof(proc_r_str), "%d", comm_w[0]);
2212+
snprintf(proc_w_str, sizeof(proc_w_str), "%d", comm_r[1]);
2213+
char *args[] = {"swaylock-sleep-watcher", proc_w_str, proc_r_str, NULL};
2214+
if (posix_spawnp(&pid, "swaylock-sleep-watcher", NULL, NULL, args, environ) == -1) {
2215+
close(comm_r[0]);
2216+
close(comm_r[1]);
2217+
close(comm_w[0]);
2218+
close(comm_w[1]);
2219+
swaylock_log(LOG_ERROR, "Failed to run sleep detection helper");
2220+
return -1;
2221+
}
2222+
close(comm_r[1]);
2223+
close(comm_w[0]);
2224+
*sleep_comm_r = comm_r[0];
2225+
*sleep_comm_w = comm_w[1];
2226+
return 0;
2227+
}
2228+
21062229
// Check for --debug 'early' we also apply the correct loglevel
21072230
// to the forked child, without having to first proces all of the
21082231
// configuration (including from file) before forking and (in the
@@ -2130,6 +2253,15 @@ void log_init(int argc, char **argv) {
21302253
}
21312254

21322255
int main(int argc, char **argv) {
2256+
/* Initially ignore SIGUSR1 + SIGUSR2 to prevent stray signals (like
2257+
* `killall -SIGUSR2`) from affecting the pw backend subprocess */
2258+
struct sigaction sa;
2259+
sa.sa_handler = SIG_IGN;
2260+
sigemptyset(&sa.sa_mask);
2261+
sa.sa_flags = 0;
2262+
sigaction(SIGUSR1, &sa, NULL);
2263+
sigaction(SIGUSR2, &sa, NULL);
2264+
21332265
log_init(argc, argv);
21342266
initialize_pw_backend(argc, argv);
21352267
srand(time(NULL));
@@ -2156,6 +2288,8 @@ int main(int argc, char **argv) {
21562288
.indicator_idle_visible = false,
21572289
.ready_fd = -1,
21582290
.plugin_command = NULL,
2291+
.grace_time = 0.0f,
2292+
.grace_pointer_hysteresis = 10.0f,
21592293
};
21602294
wl_list_init(&state.images);
21612295
set_default_colors(&state.args.colors);
@@ -2214,6 +2348,18 @@ int main(int argc, char **argv) {
22142348
swaylock_log(LOG_ERROR, "Failed to make pipe end nonblocking");
22152349
return EXIT_FAILURE;
22162350
}
2351+
if (pipe(sigusr2_fds) != 0) {
2352+
swaylock_log(LOG_ERROR, "Failed to pipe");
2353+
return EXIT_FAILURE;
2354+
}
2355+
if (!set_cloexec(sigusr2_fds[0]) || !set_cloexec(sigusr2_fds[1])) {
2356+
swaylock_log(LOG_ERROR, "Failed to make pipes close-on-exec");
2357+
return EXIT_FAILURE;
2358+
}
2359+
if (fcntl(sigusr2_fds[1], F_SETFL, O_NONBLOCK) == -1) {
2360+
swaylock_log(LOG_ERROR, "Failed to make pipe end nonblocking");
2361+
return EXIT_FAILURE;
2362+
}
22172363

22182364
// temp: make all backgrounds use some sort of plugin command
22192365
if (!state.args.plugin_command) {
@@ -2340,6 +2486,19 @@ int main(int argc, char **argv) {
23402486
daemonize();
23412487
}
23422488

2489+
state.sleep_comm_r = -1;
2490+
state.sleep_comm_w = -1;
2491+
if (state.args.grace_time > 0.) {
2492+
float delay_ms = ceilf(state.args.grace_time * 1000.f);
2493+
int delay = delay_ms >= (float)INT_MAX ? INT_MAX : (int)delay_ms;
2494+
2495+
/* Failure of this process just cancels the grace period */
2496+
if (start_sleep_watcher(&state.sleep_comm_r, &state.sleep_comm_w) == 0) {
2497+
state.grace_timer = loop_add_timer(state.eventloop, delay, grace_timeout, &state);
2498+
loop_add_fd(state.eventloop, state.sleep_comm_r, POLLIN, sleep_in, &state);
2499+
}
2500+
}
2501+
23432502
/* fill in dmabuf modifier list if empty and upstream provided dmabuf-feedback */
23442503
if (state.forward.linux_dmabuf && zwp_linux_dmabuf_v1_get_version(state.forward.linux_dmabuf) >= 4) {
23452504
size_t npairs = 0;
@@ -2420,13 +2579,18 @@ int main(int argc, char **argv) {
24202579
POLLIN, dispatch_nested, NULL);
24212580

24222581
loop_add_fd(state.eventloop, sigusr_fds[0], POLLIN, term_in, NULL);
2582+
loop_add_fd(state.eventloop, sigusr2_fds[0], POLLIN, lock_in, NULL);
24232583

2424-
struct sigaction sa;
24252584
sa.sa_handler = do_sigusr;
24262585
sigemptyset(&sa.sa_mask);
24272586
sa.sa_flags = SA_RESTART;
24282587
sigaction(SIGUSR1, &sa, NULL);
24292588

2589+
sa.sa_handler = do_sigusr2;
2590+
sigemptyset(&sa.sa_mask);
2591+
sa.sa_flags = SA_RESTART;
2592+
sigaction(SIGUSR2, &sa, NULL);
2593+
24302594
// Ignore SIGCHLD, to make child processes be automatically reaped.
24312595
// (This setting is not inherited to child processes.)
24322596
struct sigaction sa2;

meson.build

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ libpam = cc.find_library('pam', required: get_option('pam'))
4040
crypt = cc.find_library('crypt', required: not libpam.found())
4141
math = cc.find_library('m')
4242
rt = cc.find_library('rt')
43+
logind = dependency('lib' + get_option('logind-provider'), required: get_option('logind'))
4344

4445
git = find_program('git', required: false)
4546
scdoc = find_program('scdoc', required: get_option('man-pages'))
@@ -110,6 +111,11 @@ conf_data = configuration_data()
110111
conf_data.set_quoted('SYSCONFDIR', get_option('prefix') / get_option('sysconfdir'))
111112
conf_data.set_quoted('SWAYLOCK_VERSION', version)
112113
conf_data.set10('HAVE_GDK_PIXBUF', gdk_pixbuf.found())
114+
conf_data.set10('HAVE_SYSTEMD', false)
115+
conf_data.set10('HAVE_ELOGIND', false)
116+
if logind.found()
117+
conf_data.set10('HAVE_' + get_option('logind-provider').to_upper(), true)
118+
endif
113119

114120
subdir('include')
115121

@@ -159,6 +165,13 @@ executable('swaylock-plugin',
159165
install: true
160166
)
161167

168+
executable('swaylock-sleep-watcher',
169+
['sleep-watcher.c', 'log.c'],
170+
include_directories: [swaylock_inc],
171+
dependencies: [logind],
172+
install: true
173+
)
174+
162175
if libpam.found()
163176
install_data(
164177
'pam/swaylock-plugin',

meson_options.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ option('man-pages', type: 'feature', value: 'auto', description: 'Generate and i
44
option('zsh-completions', type: 'boolean', value: true, description: 'Install zsh shell completions')
55
option('bash-completions', type: 'boolean', value: true, description: 'Install bash shell completions')
66
option('fish-completions', type: 'boolean', value: true, description: 'Install fish shell completions')
7+
option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind (to automatically end grace period on sleep)')
8+
option('logind-provider', type: 'combo', choices: ['systemd', 'elogind'], value: 'systemd', description: 'Provider of logind support library')

0 commit comments

Comments
 (0)