@@ -0,0 +1,327 @@
.\" Macros from groff an-ext.tmac file for portability
.de TQ
. ns
. TP \\$1\" no doublequotes around argument!
..
.de UR
. ds m1 \\$1\"
. nh
. if \\n(mH \{\
. do ev URL-div
. do di URL-div
. \}
..
.de UE
. ie \\n(mH \{\
. br
. di
. ev
.
. ie \\n(dn \{\
. do HTML-NS "<a href=""\\*(m1"">"
. do chop URL-div
. do URL-div
\c
. do HTML-NS </a>
. \}
. el \
. do HTML-NS "<a href=""\\*(m1"">\\*(m1</a>"
\&\\$*\"
. \}
. el \
\\*(la\\*(m1\\*(ra\\$*\"
.
. hy \\n(HY
..
.de EX
. do ds mF \\n[.fam]
. nr mE \\n(.f
. nf
. nh
. do fam C
. ft CW
..
.de EE
. do fam \\*(mF
. ft \\n(mE
. fi
. hy \\n(HY
..
.
.
.TH XSUSPENDER 1 2017-12-13
.SH NAME
xsuspender \- auto-suspend inactive X11 applications
.SH SYNOPSIS
.TP
.B xsuspender
.SH DESCRIPTION
Modern desktop GUI applications may use up significant system resources,
such as CPU time, just while idly running and even when unused.
Respecting expressive configuration, XSuspender moderates such
applications to suspend all activity shortly after their windows are unfocused.
by sending them Unix signal
.B SIGSTOP
(or executing a custom script; see
.B exec_suspend
key in section
.B CONFIGURATION
below).
When the application window regains focus, XSuspender sends it signal
.B SIGCONT\fR,
and it resumes execution exactly where it had left off.
.PP
This may result in significant reduction in CPU use which
prolongs battery run-time (5\-500%; subject to user's computing habits),
reduces CPU fan noise, and
prevents resource-hogging applications from plotting stuff
behind your back at least when you are not actively using them.
.PP
The method also has its share of drawbacks. See section
.B BUGS
below.
.SH OPTIONS
The program is mainly run without arguments.
.TP
.BR \-h ", " \-\-help
Print short help and exit.
.SH ENVIRONMENT
.TP
.B G_MESSAGES_DEBUG
Set
.B G_MESSAGES_DEBUG\fR=\fIall\fR
to have the program report debug messages of its activity
to standard error.
.TP
.B XDG_CONFIG_HOME
If set, the directory where
.I xsuspender.conf
configuration file can be found.
If missing, this location defaults to \fI~/.config\fR.
.SH FILES
.TP
.I ~/.config/xsuspender.conf
Configuration file read by the program.
.TP
.I $XDG_CONFIG_HOME/xsuspender.conf
Alternative configuration file when the variable is set.
.TP
.I share/xsuspender/xsuspender.conf.example
Example configuration file shipped with the program.
.TP
.I etc/xdg/autostart/xsuspender.desktop
A desktop session auto-start launcher.
Disabled by default.
To enable, copy it into
.I .config/autostart
directory (or
.I $XDG_CONFIG_HOME/autostart
where applicable) and edit it, setting \fIHidden=false\fR.
Alternatively, you may be able to enable it via the
session application auto-start manager.
.SH CONFIGURATION
Configuration key\-value file is in INI format. Notice:
.RS 2
.IP \[bu] 2
file must be encoded in UTF-8,
.IP \[bu] 2
comments follow '#' character,
.IP \[bu] 2
section and key names are case-sensitive.
.RE
.PP
The file should contain a
.B [Default]
section which all other sections inherit and override on per-key basis.
.PP
Each non-\fB[Default]\fR section constitutes a rule that
XSuspender will try to match and follow.
Sections can define the the following keys (listed with built-in
default values):
.TP
.B match_wm_name_contains
.TQ
.B match_wm_class_contains
.TQ
.B match_wm_class_group_contains
These keys order xsuspender to apply the rule only to windows
that contain the value string verbatim in their
.I WM_NAME
and
.I WM_CLASS
window manager hints.
When multiple matching keys are specified, their
logical conjunction (intersection) applies.
To find applicable values, run:
.IP
.in +4
.nf
xprop -notype WM_NAME WM_CLASS
.fi
.in
.IP
and click on the window of interest. Class group is the
second string in the
.I WM_CLASS
atom.
.TP
.BR suspend_delay \ =\ 10
Integer seconds to wait after the window loses focus
before it is suspended.
Setting a reasonable value avoids suspending processes too early during
rapid task switching.
If you set this value too low, upon process termination,
the process may stall in its termination routine if it hasn't
been able to finish in time.
Since we're unable to detect this, best give your processes ample time.
.TP
.BR resume_every \ =\ 50
Resume the suspended process every this many integer seconds
to have it do (some of) its backlog of work.
.TP
.BR resume_for \ =\ 5
Resume suspended process only for this long before suspending it again.
.TP
.BR only_on_battery \ =\ true
Apply the rule only when on battery power. You may wish to set this to
\fIfalse\fR for hopelessly annoying processes such as virtual machines
running Windos.
.TP
.BR auto_suspend_on_battery \ =\ true
If true, automatically apply this rule when on batter power, even if
the matching window didn't just lose focus.
.TP
.BR suspend_subtree_pattern
Extended regular expression.
If provided, when XSuspender suspends a process, it will also suspend
all its descendant processes (children, grandchildren etc.) whose
command line string matches the pattern.
Set to \fI.\fR to match and kill all descendants.
This setting only applies when
.B send_signals
is also true.
.TP
.BR exec_suspend
.TQ
.BR exec_resume
Before suspending / resuming the process,
execute the string as a command line shell script.
If the script execution is successful (exit status 0), proceed
with process suspension, otherwise abort it.
(Process resumption is never aborted.)
.IP
The executed script gets passed via the environment the following
variables:
.I PID
(process id),
.I XID
(X11 window id of the triggering window), and
.I WM_NAME
(window title).
.IP
You can use these keys to control suspension of your processes
or disjoint components thereof
in a custom way. In this case, you may also find
.B send_signals
key to be of use.
.TP
.BR send_signals \ =\ true
If true, XSuspender kills the matching process with
SIGSTOP / SIGCONT Unix signals.
If false, XSuspender sends no signals of its own, deferring to
your
.B exec_suspend
and
.B exec_resume
scripts to do the work.
.SH BUGS
The following limitations and quirks are known:
.IP \[bu] 2
Don't configure XSuspender for software you want to keep continuously alive
in the background, such as music players and daemons.
If you suspend IM clients, wake them up frequently (configuration key
.B resume_every\fR)
so they can process their queued network events.
If you like to stream music from YouTube or similar, you might
want to exempt your browser from suspension,
or consider using a lighter-weight native client, such as Musictube.
.IP \[bu] 2
Windows that minimize to system tray, such as torrent and IM clients,
need to be resumed frequently to respond to tray icon click events
in a seamless manner.
.IP \[bu] 2
Pasting from clipboard is prevented while the selection source process
is suspended.
Mitigate by using a clipboard manager that takes ownership of
the selection.
.IP \[bu] 2
Suspended windows might not redraw until regaining focus.
If something covers them, expect visual artifacts.
.IP \[bu] 2
Mouse-wheel scrolling might not work in suspended windows.
They require keyboard input focus to resume.
.IP \[bu] 2
Processes that take a long time to shut down after their window already
disappears may be stopped in the middle of their termination routines.
Avoid with reasonably generous `suspend_delay`.
.IP \[bu] 2
XSuspender won't work in remote X sessions.
.IP \[bu] 2
XSuspender won't work with Wayland.
.PP
Please raise any further bugs and ideas on the project's
.UR https://github.com/kernc/xsuspender/issues
GitHub issue tracker
.UE .
.SH EXAMPLE
Example configuration section for VirtualBox:
.PP
.in +4
.EX
# This is a comment.
[Default]
suspend_delay = 10
resume_every = 50
resume_for = 5
only_on_battery = true
auto_suspend_on_battery = true
send_signals = true
# Rule name is an alphanumeric string.
# Rule VirtualBox inherits from Default rule
[VirtualBox]
match_wm_class_contains = VirtualBox
# VirtualBox suspension is managed by VBoxManage utility.
send_signals = false
# We get the VM UUID via its process command line.
# NOTE: Unlike in this example, the whole script should be
# on a single line.
exec_suspend = VBoxManage controlvm "$(ps -o args= -q $PID | \\
sed -E 's/.*--startvm ([a-f0-9-]+).*/\\1/')" pause
exec_resume = VBoxManage controlvm "$(ps -o args= -q $PID | \\
sed -E 's/.*--startvm ([a-f0-9-]+).*/\\1/')" resume
.EE
.in
.PP
Find further examples in example configuration file
accompanying this distribution
(see section
.B FILES
above).
.PP
To debug your configuration rules, run:
.PP
.in +4
.nf
G_MESSAGES_DEBUG=all xsuspender
.fi
.in
.SH SEE ALSO
.BR kill (1),
.BR xprop (1),
.BR regex (7),
.BR signal (7)
19 lint.sh
@@ -0,0 +1,19 @@
#!/bin/sh

set -e

ROOT="$(dirname "$0")"
PATH="$PATH:$HOME/.local/bin"

error () { echo "ERROR: $@" >&2; exit 1; }

cppcheck --enable=all --error-exitcode=1 \
--suppress=unusedFunction:"$ROOT/src/macros.h" "$ROOT"/src/*.[ch]

cpplint --counting=detailed --recursive "$ROOT"/src

grep -RF $'\t' "$ROOT"/src &&
error 'Tabs found. Use spaces.' || echo 'Whitespace OK'

grep -RP '[^[:cntrl:][:print:]]' "$ROOT"/src &&
error 'No emojis.' || echo 'Emojis OK'
@@ -0,0 +1,19 @@
find_package (PkgConfig REQUIRED)

pkg_check_modules (GLIB REQUIRED glib-2.0)
pkg_check_modules (WNCK REQUIRED libwnck-3.0)

add_definitions (-DWNCK_I_KNOW_THIS_IS_UNSTABLE
-D_POSIX_SOURCE)

add_compile_options (${GLIB_CFLAGS} ${WNCK_CFLAGS})

file (GLOB SOURCES "*.[ch]")
add_executable (${PROJECT_NAME} ${SOURCES})

# pkg-config recipe for libwnck may be a bit overzealos; trim linked libs to essentials
set (CMAKE_EXE_LINKER_FLAGS "-Wl,--as-needed -Wl,--no-undefined")

target_link_libraries (${PROJECT_NAME} PRIVATE ${GLIB_LDFLAGS} ${WNCK_LDFLAGS})

install (TARGETS ${PROJECT_NAME} DESTINATION bin)
@@ -0,0 +1,272 @@
#include "config.h"

#include <stdlib.h>

#include <glib.h>

#include "rule.h"
#include "macros.h"


static
gboolean error_encountered = FALSE;


static
gboolean
is_error (GError **err)
{
if (g_error_matches (*err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE)) {
g_warning ("Error parsing config: %s", (*err)->message);
error_encountered = TRUE;
}
if (*err) {
g_error_free (*err);
*err = NULL;
return TRUE;
}
return FALSE;
}


static
char**
parse_command (GKeyFile *file,
char *section,
char *key,
GError **err)
{
g_autofree char *value = g_key_file_get_value (file, section, key, err);
if (! value)
return NULL;

char **argv = NULL;
// Empty command is same as no command
value = g_strstrip (value);
if (g_strcmp0 (value, "") != 0) {
// Pass everything to /bin/sh as this is the most convenient for scripting
char *args[4] = {"sh", "-c", value, NULL};
argv = g_strdupv (args);
}
return argv;
}


static
void
reassign_strv (char ***existing,
char **replacement)
{
// This can be simple because the rest is done in parse_command()
g_strfreev (*existing);
*existing = replacement;
}


static
void
reassign_str (char **existing,
char *replacement)
{
// If the key was missing, GError should catch it.
// Perhaps an empty one, but replacement is definitely a string.
g_assert (replacement != NULL);

// Free the previous value
g_free (*existing);
*existing = NULL;

// If replacement is a non-empty string, use it
if (g_strcmp0 (replacement, "") != 0)
*existing = replacement;
else
// Otherwise, we have no use for it
g_free (replacement);
}


// Remove once glib-2.44+ is ubiquitous
#if GLIB_MINOR_VERSION < 44
static inline
gboolean
g_strv_contains (const gchar *const *strv,
const gchar *str)
{
for (; *strv; strv++)
if (g_strcmp0 (str, *strv) == 0)
return TRUE;
return FALSE;
}
#endif


static
void
read_section (GKeyFile *file,
char *section,
Rule *rule)
{
g_autoptr (GError) err = NULL;

int val;

val = g_key_file_get_boolean (file, section, CONFIG_KEY_ONLY_ON_BATTERY, &err);
if (! is_error (&err)) rule->only_on_battery = (gboolean) val;

val = g_key_file_get_boolean (file, section, CONFIG_KEY_AUTO_ON_BATTERY, &err);
if (! is_error (&err)) rule->auto_on_battery = (gboolean) val;

val = g_key_file_get_boolean (file, section, CONFIG_KEY_SEND_SIGNALS, &err);
if (! is_error (&err)) rule->send_signals = (gboolean) val;

val = g_key_file_get_integer (file, section, CONFIG_KEY_SUSPEND_DELAY, &err);
if (! is_error (&err)) rule->delay = (guint16) CLAMP (val, 1, G_MAXUINT16);

val = g_key_file_get_integer (file, section, CONFIG_KEY_RESUME_EVERY, &err);
if (! is_error (&err)) rule->resume_every = (guint16) (val ? CLAMP (val, 1, G_MAXUINT16) : 0);

val = g_key_file_get_integer (file, section, CONFIG_KEY_RESUME_FOR, &err);
if (! is_error (&err)) rule->resume_for = (guint16) CLAMP (val, 1, G_MAXUINT16);

char *str;

str = g_key_file_get_value (file, section, CONFIG_KEY_WM_CLASS_CONTAINS, &err);
if (! is_error (&err)) reassign_str (&rule->needle_wm_class, str);

str = g_key_file_get_value (file, section, CONFIG_KEY_WM_CLASS_GROUP_CONTAINS, &err);
if (! is_error (&err)) reassign_str (&rule->needle_wm_class_group, str);

str = g_key_file_get_value (file, section, CONFIG_KEY_WM_NAME_CONTAINS, &err);
if (! is_error (&err)) reassign_str (&rule->needle_wm_name, str);

str = g_key_file_get_value (file, section, CONFIG_KEY_SUBTREE_PATTERN, &err);
if (! is_error (&err)) reassign_str (&rule->subtree_pattern, str);

char **argv;

argv = parse_command (file, section, CONFIG_KEY_EXEC_SUSPEND, &err);
if (! is_error (&err)) reassign_strv (&rule->exec_suspend, argv);

argv = parse_command (file, section, CONFIG_KEY_EXEC_RESUME, &err);
if (! is_error (&err)) reassign_strv (&rule->exec_resume, argv);

g_assert (err == NULL); // Already freed

// Ensure all configuration keys are valid
static const char* VALID_KEYS[] = CONFIG_VALID_KEYS;
g_auto (GStrv) keys = g_key_file_get_keys (file, section, NULL, NULL);
for (int i = 0; keys[i]; ++i) {
if (! g_strv_contains (VALID_KEYS, keys[i])) {
g_warning ("Invalid key in section '%s': %s", section, keys[i]);
error_encountered = TRUE;
}
}

// For non-Default sections, ensure at least one window-matching needle is specified
if (g_strcmp0 (section, CONFIG_DEFAULT_SECTION) != 0 &&
! rule->needle_wm_name &&
! rule->needle_wm_class &&
! rule->needle_wm_class_group) {
g_warning ("Invalid rule '%s' matches all windows", section);
error_encountered = TRUE;
}
}


static
void
debug_print_rule (Rule *rule)
{
g_debug ("\n"
"needle_wm_class = %s\n"
"needle_wm_class_group = %s\n"
"needle_wm_name = %s\n"
"delay = %d\n"
"resume_every = %d\n"
"resume_for = %d\n"
"only_on_battery = %d\n"
"send_signals = %d\n"
"subtree_pattern = %s\n"
"exec_suspend = %s\n"
"exec_resume = %s\n",
rule->needle_wm_class,
rule->needle_wm_class_group,
rule->needle_wm_name,
rule->delay,
rule->resume_every,
rule->resume_for,
rule->only_on_battery,
rule->send_signals,
rule->subtree_pattern,
(rule->exec_suspend ? rule->exec_suspend[2] : NULL),
(rule->exec_resume ? rule->exec_resume[2] : NULL)
);
}


Rule **
parse_config ()
{
Rule defaults = {
.needle_wm_name = NULL,
.needle_wm_class = NULL,
.needle_wm_class_group = NULL,

.exec_suspend = NULL,
.exec_resume = NULL,

.subtree_pattern = NULL,

.delay = 10,
.resume_every = 50,
.resume_for = 5,

.send_signals = TRUE,
.only_on_battery = TRUE,
.auto_on_battery = TRUE,
};

g_autoptr (GKeyFile) file = g_key_file_new ();
g_autoptr (GError) err = NULL;

g_autofree char *path = g_build_path ("/", g_get_user_config_dir (), CONFIG_FILE_NAME, NULL);
g_key_file_load_from_file (file, path, G_KEY_FILE_NONE, &err);

if (err)
g_critical ("Cannot read configuration file '%s': %s", path, err->message);

// Process Default section
gboolean has_default_section = g_key_file_has_group (file, CONFIG_DEFAULT_SECTION);
if (has_default_section)
read_section (file, CONFIG_DEFAULT_SECTION, &defaults);

// Read all other sections (rules)
gsize n_sections = 0;
g_auto (GStrv) sections = g_key_file_get_groups (file, &n_sections);
n_sections -= has_default_section;

if (n_sections <= 0)
g_critical ("No configuration rules found. Nothing to do. Exiting.");

Rule **rules = g_malloc0_n (n_sections + 1, sizeof (Rule*)),
**ptr = rules;

for (int i = 0; sections[i]; ++i) {
// Skip Default section; it was already handled above
if (g_strcmp0 (sections[i], CONFIG_DEFAULT_SECTION) == 0)
continue;

Rule *rule = xsus_rule_copy (&defaults);
read_section (file, sections[i], rule);
*ptr++ = rule;
}

// Debug dump rules in effect
for (int i = 0; rules[i]; ++i)
debug_print_rule (rules[i]);

if (error_encountered)
g_critical ("Errors encountered while parsing config. Abort.");

return rules;
}
@@ -0,0 +1,42 @@
#ifndef XSUSPENDER_PARSE_CONFIG_H
#define XSUSPENDER_PARSE_CONFIG_H

#include "xsuspender.h"
#include "rule.h"

#define CONFIG_FILE_NAME (PROJECT_NAME ".conf")

#define CONFIG_DEFAULT_SECTION "Default"

#define CONFIG_KEY_EXEC_SUSPEND "exec_suspend"
#define CONFIG_KEY_EXEC_RESUME "exec_resume"
#define CONFIG_KEY_SUSPEND_DELAY "suspend_delay"
#define CONFIG_KEY_RESUME_EVERY "resume_every"
#define CONFIG_KEY_RESUME_FOR "resume_for"
#define CONFIG_KEY_ONLY_ON_BATTERY "only_on_battery"
#define CONFIG_KEY_AUTO_ON_BATTERY "auto_suspend_on_battery"
#define CONFIG_KEY_SEND_SIGNALS "send_signals"
#define CONFIG_KEY_SUBTREE_PATTERN "suspend_subtree_pattern"
#define CONFIG_KEY_WM_NAME_CONTAINS "match_wm_name_contains"
#define CONFIG_KEY_WM_CLASS_CONTAINS "match_wm_class_contains"
#define CONFIG_KEY_WM_CLASS_GROUP_CONTAINS "match_wm_class_group_contains"

#define CONFIG_VALID_KEYS {CONFIG_KEY_ONLY_ON_BATTERY, \
CONFIG_KEY_AUTO_ON_BATTERY, \
CONFIG_KEY_SEND_SIGNALS, \
CONFIG_KEY_SUBTREE_PATTERN, \
CONFIG_KEY_SUSPEND_DELAY, \
CONFIG_KEY_RESUME_EVERY, \
CONFIG_KEY_RESUME_FOR, \
CONFIG_KEY_WM_CLASS_CONTAINS, \
CONFIG_KEY_WM_CLASS_GROUP_CONTAINS, \
CONFIG_KEY_WM_NAME_CONTAINS, \
CONFIG_KEY_EXEC_SUSPEND, \
CONFIG_KEY_EXEC_RESUME, \
NULL}


Rule** parse_config ();


#endif // XSUSPENDER_PARSE_CONFIG_H
@@ -0,0 +1,63 @@
#include "entry.h"

#include <glib.h>
#include <libwnck/libwnck.h>


WindowEntry*
xsus_window_entry_new (WnckWindow *window,
Rule *rule)
{
WindowEntry *entry = g_malloc (sizeof (WindowEntry));
entry->rule = rule;
entry->pid = wnck_window_get_pid (window);
entry->xid = wnck_window_get_xid (window);
entry->wm_name = g_strdup (wnck_window_get_name (window));
return entry;
}


WindowEntry*
xsus_window_entry_copy (WindowEntry *entry)
{
WindowEntry *copy = g_memdup (entry, sizeof (WindowEntry));
copy->wm_name = g_strdup (copy->wm_name);
return copy;
}


void
xsus_window_entry_free (WindowEntry *entry)
{
if (! entry)
return;
g_free (entry->wm_name);
g_free (entry);
}


inline
WindowEntry*
xsus_entry_find_for_window_rule (WnckWindow *window,
Rule *rule,
GSList *list)
{
// If suspending by signals, find entry by PID ...
if (rule->send_signals) {
pid_t pid = wnck_window_get_pid (window);
for (; list; list = list->next) {
WindowEntry *entry = list->data;
if (entry->pid == pid)
return entry;
}
} else {
// ... else find it by XID
XID xid = wnck_window_get_xid (window);
for (; list; list = list->next) {
WindowEntry *entry = list->data;
if (entry->xid == xid)
return entry;
}
}
return NULL;
}
@@ -0,0 +1,36 @@
#ifndef XSUSPENDER_ENTRY_H
#define XSUSPENDER_ENTRY_H

#include <sys/types.h>

#include <glib.h>
#include <libwnck/libwnck.h>

#include "rule.h"


typedef gulong XID;


// Window entry in the hash tables
typedef struct WindowEntry {
Rule *rule;
char *wm_name; // Window title

// The X ID and PID of the window that triggered the rule
XID xid;
pid_t pid;

// Timestamp of the next (and last) window suspension by this rule.
time_t suspend_timestamp;
} WindowEntry;


WindowEntry* xsus_window_entry_new (WnckWindow *window, Rule *rule);
WindowEntry* xsus_window_entry_copy (WindowEntry *entry);
void xsus_window_entry_free (WindowEntry *entry);

WindowEntry* xsus_entry_find_for_window_rule (WnckWindow *window, Rule *rule, GSList *where);


#endif // XSUSPENDER_ENTRY_H
@@ -0,0 +1,236 @@
#include "events.h"

#include <glib.h>

#include <libwnck/libwnck.h>
#include <time.h>

#include "entry.h"


void
xsus_init_event_handlers ()
{
WnckScreen *screen = wnck_screen_get_default ();

g_signal_connect (screen, "active-window-changed",
G_CALLBACK (on_active_window_changed), NULL);

// Periodically run handler tending to the suspension queue
g_timeout_add_seconds_full (
G_PRIORITY_LOW, SUSPEND_PENDING_INTERVAL, on_suspend_pending_windows, NULL, NULL);

// Periodically resume windows for a while so they get "up to date"
g_timeout_add_seconds_full (
G_PRIORITY_LOW, PERIODIC_RESUME_INTERVAL, on_periodic_window_wake_up, NULL, NULL);

// Periodically check if we're on battery power
g_timeout_add_seconds_full (
G_PRIORITY_LOW, CHECK_BATTERY_INTERVAL, on_check_battery_powered, NULL, NULL);
}


static
WnckWindow*
get_main_window (WnckWindow *window)
{
if (! WNCK_IS_WINDOW (window))
return NULL;

// Resolve transient (dependent, dialog) windows
WnckWindow *parent;
while ((parent = wnck_window_get_transient (window)))
window = parent;

// Ensure window is of correct type
WnckWindowType type = wnck_window_get_window_type (window);
if (type == WNCK_WINDOW_NORMAL ||
type == WNCK_WINDOW_DIALOG)
return window;

return NULL;
}


static inline
gboolean
windows_are_same_process (WnckWindow *w1,
WnckWindow *w2)
{
// Consider windows to be of the same process when they
// are one and the same window,
if (w1 == w2)
return TRUE;

// Or when they have the same PID, map to the same rule,
// and the rule says that signals should be sent.
Rule *rule;
return (WNCK_IS_WINDOW (w1) &&
WNCK_IS_WINDOW (w2) &&
wnck_window_get_pid (w1) == wnck_window_get_pid (w2) &&
(rule = xsus_window_get_rule (w1)) &&
rule->send_signals &&
rule == xsus_window_get_rule (w2));
}


void
on_active_window_changed (WnckScreen *screen,
WnckWindow *prev_active_window)
{
WnckWindow *active_window = wnck_screen_get_active_window (screen);

active_window = get_main_window (active_window);
prev_active_window = get_main_window (prev_active_window);

// Main windows are one and the same; do nothing
if (windows_are_same_process (active_window, prev_active_window))
return;

// Resume the active window if it was (to be) suspended
if (active_window)
xsus_window_resume (active_window);

// Maybe suspend previously active window
if (prev_active_window)
xsus_window_suspend (prev_active_window);
}


static inline
gboolean
window_exists (WindowEntry *entry)
{
WnckWindow *window = wnck_window_get (entry->xid);
return window && wnck_window_get_pid (window) == entry->pid;
}


int
on_suspend_pending_windows ()
{
time_t now = time (NULL);
GSList *l = queued_entries;
while (l) {
GSList *next = l->next;
WindowEntry *entry = l->data;

if (now >= entry->suspend_timestamp) {
queued_entries = g_slist_delete_link (queued_entries, l);

// Follow through with suspension only if window is still alive
if (window_exists (entry))
xsus_signal_stop (entry);
}
l = next;
}
return TRUE;
}


int
on_periodic_window_wake_up ()
{
time_t now = time (NULL);
GSList *l = suspended_entries;
while (l) {
WindowEntry *entry = l->data;
l = l->next;

// Is it time to resume the process?
if (entry->rule->resume_every &&
now - entry->suspend_timestamp >= entry->rule->resume_every) {
g_debug ("Periodic awaking %#lx (%d) for %d seconds",
entry->xid, entry->pid, entry->rule->resume_for);

// Re-schedule suspension if window is still alive
if (window_exists (entry)) {
// Make a copy because continuing below frees the entry
WindowEntry *copy = xsus_window_entry_copy (entry);
xsus_window_entry_enqueue (copy, entry->rule->resume_for);
}

xsus_signal_continue (entry);
}
}
return TRUE;
}


static
void
iterate_windows_kill_matching ()
{
for (GList *w = wnck_screen_get_windows (wnck_screen_get_default ()); w ; w = w->next) {
WnckWindow *window = w->data;

// Skip transient windows and windows of incorrect type
if (window != get_main_window (window))
continue;

Rule *rule = xsus_window_get_rule (window);

// Skip non-matching windows
if (! rule)
continue;

// Skip currently focused window
if (wnck_window_is_active (window))
continue;

// On battery, auto-suspend windows that allow it
if (is_battery_powered && rule->auto_on_battery) {
// Do nothing if we're already keeping track of this window
if (xsus_entry_find_for_window_rule (window, rule, queued_entries) ||
xsus_entry_find_for_window_rule (window, rule, suspended_entries))
continue;

// Otherwise, schedule the window for suspension shortly
WindowEntry *entry = xsus_window_entry_new (window, rule);
xsus_window_entry_enqueue (entry, rule->resume_for);

// On AC, don't auto-resume windows that want to be suspended also
// when on AC, e.g. VirtualBox with Windos
} else if (!is_battery_powered && rule->only_on_battery) {
xsus_window_resume (window);
}
}
}


static
gboolean
is_on_ac_power ()
{
#ifdef __linux__
int exit_status = -1;
// Read AC power state. Should work in most cases. See: https://bugs.debian.org/473629
char *argv[] = {"sh", "-c", "grep -q 1 /sys/class/power_supply/*/online", NULL};
g_spawn_sync (NULL, argv, NULL,
G_SPAWN_SEARCH_PATH | (IS_DEBUG ? G_SPAWN_DEFAULT : G_SPAWN_STDERR_TO_DEV_NULL),
NULL, NULL, NULL, NULL, &exit_status, NULL);
gboolean is_ac_power = exit_status == 0;
return is_ac_power;
#else
#warning "No battery / AC status support for your platform."
#warning "Defaulting to as if 'always on battery' behavior. Patches welcome!"
return FALSE;
#endif
}


int
on_check_battery_powered ()
{
gboolean previous_state = is_battery_powered;
is_battery_powered = ! is_on_ac_power ();

// On battery state change, suspend / resume matching windows
if (previous_state != is_battery_powered) {
g_debug ("AC power = %d; State changed. Suspending/resuming windows.",
! is_battery_powered);

iterate_windows_kill_matching ();
}
return TRUE;
}
@@ -0,0 +1,20 @@
#ifndef XSUSPENDER_EVENT_HANDLERS_H
#define XSUSPENDER_EVENT_HANDLERS_H

#include "xsuspender.h"


#define SUSPEND_PENDING_INTERVAL 1
#define PERIODIC_RESUME_INTERVAL 1
#define CHECK_BATTERY_INTERVAL 10


void xsus_init_event_handlers ();

void on_active_window_changed (WnckScreen *screen, WnckWindow *prev_active_window);
int on_suspend_pending_windows ();
int on_periodic_window_wake_up ();
int on_check_battery_powered ();


#endif // XSUSPENDER_EVENT_HANDLERS_H
@@ -0,0 +1,98 @@
#include "exec.h"

#include <signal.h>
#include <sys/types.h>

#include "macros.h"


static inline
int
execute (char **argv,
char **envp)
{
g_autoptr (GError) err = NULL;
gint exit_status = -1;
GSpawnFlags flags = IS_DEBUG ?
G_SPAWN_DEFAULT :
G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL;

g_spawn_sync (NULL, argv, envp, flags | G_SPAWN_SEARCH_PATH,
NULL, NULL, NULL, NULL, &exit_status, &err);
if (err)
g_warning ("Unexpected subprocess execution error: %s", err->message);

return exit_status;
}


int
xsus_exec_subprocess (char **argv,
WindowEntry *entry)
{
if (! argv)
return 0;

// Provide window data to subprocess via the environment
char *envp[] = {
g_strdup_printf ("PID=%d", entry->pid),
g_strdup_printf ("XID=%#lx", entry->xid),
g_strdup_printf ("WM_NAME=%s", entry->wm_name),
g_strdup_printf ("PATH=%s", g_getenv ("PATH")),
g_strdup_printf ("LC_ALL=C"), // Speeds up locale-aware shell utils
NULL
};

// Execute and return result
g_debug ("Exec %#lx (%d): %s", entry->xid, entry->pid, argv[2]);
int exit_status = execute (argv, envp);
g_debug ("Exit status: %d", exit_status);

// Free envp
char **e = envp; while (*e) g_free(*e++);

return exit_status;
}


int
xsus_kill_subtree (pid_t pid,
int signal,
char *cmd_pattern)
{
if (! cmd_pattern)
return 0;

g_assert (signal == SIGSTOP || signal == SIGCONT);
char *sig = signal == SIGSTOP ? "STOP" : "CONT";

// Prefer this short script to using pstree (induces extra dependency)
// or involved parsing of /proc
g_autofree char *script = g_strdup_printf (
"pid=%d\n"
"ps_output=\"$(ps -e -o ppid,pid,cmd | awk \"/%s/\"'{ print $1, $2 }')\"\n"
"while [ \"$pid\" ]; do\n"
" pid=\"$(echo \"$ps_output\" | awk \"/^($pid) /{ print \\$2 }\")\"\n"
" echo -n $pid\" \"\n"
" pid=\"$(echo \"$pid\" | paste -sd '|' -)\"\n"
"done%s | xargs kill -%s 2>/dev/null",
pid, cmd_pattern, (IS_DEBUG ? " | tee /dev/fd/2" : ""), sig);

char *argv[] = {
"sh", "-c", script, NULL};
char *envp[] = {
g_strdup_printf ("PATH=%s", g_getenv ("PATH")),
g_strdup_printf ("LC_ALL=C"), // Speeds up locale-aware awk
NULL
};

// Execute and return result
g_debug ("Exec: pstree %d | kill -%s", pid, sig);
int exit_status = execute (argv, envp);
g_debug ("Exit status: %d", exit_status);

// Free envp
char **e = envp; while (*e) g_free(*e++);

return exit_status;
}
@@ -0,0 +1,12 @@
#ifndef XSUSPENDER_SUBPROCESS_H
#define XSUSPENDER_SUBPROCESS_H

#include <sys/types.h>

#include "xsuspender.h"


int xsus_exec_subprocess (char **argv, WindowEntry *entry);
int xsus_kill_subtree (pid_t pid, int signal, char *cmd_pattern);

#endif // XSUSPENDER_SUBPROCESS_H
@@ -0,0 +1,43 @@
#ifndef XSUSPENDER_MACROS_H
#define XSUSPENDER_MACROS_H

#include <glib.h>

// Remove once glib-2.44+ is ubiquitous
#if !defined(g_auto) || \
!defined(g_autoptr) || \
!defined(g_autofree)

static inline void
_cleanup_generic_autofree (void *p)
{
void **pp = (void**) p;
if (*pp)
g_free (*pp);
}

#define _GNUC_CLEANUP(func) __attribute__((cleanup(func)))
#define _AUTOPTR_FUNC_NAME(TypeName) _xsus_autoptr_cleanup_##TypeName
#define _AUTOPTR_TYPENAME(TypeName) TypeName##_autoptr
#define _AUTO_FUNC_NAME(TypeName) _xsus_auto_cleanup_##TypeName
#define _DEFINE_AUTOPTR_CLEANUP_FUNC(TypeName, func) \
typedef TypeName *_AUTOPTR_TYPENAME(TypeName); \
static inline void _AUTOPTR_FUNC_NAME(TypeName) (TypeName **_ptr) { if (*_ptr) (func) (*_ptr); }
#define _DEFINE_AUTO_CLEANUP_FREE_FUNC(TypeName, func) \
static inline void _AUTO_FUNC_NAME(TypeName) (TypeName *_ptr) { if (*_ptr) (func) (*_ptr); }

_DEFINE_AUTOPTR_CLEANUP_FUNC(GError, g_error_free)
_DEFINE_AUTOPTR_CLEANUP_FUNC(GKeyFile, g_key_file_unref)
_DEFINE_AUTOPTR_CLEANUP_FUNC(GOptionContext, g_option_context_free)
_DEFINE_AUTO_CLEANUP_FREE_FUNC(GStrv, g_strfreev)
#undef _DEFINE_AUTO_CLEANUP_FREE_FUNC
#undef _DEFINE_AUTOPTR_CLEANUP_FUNC

#define g_autoptr(TypeName) _GNUC_CLEANUP(_AUTOPTR_FUNC_NAME(TypeName)) _AUTOPTR_TYPENAME(TypeName)
#define g_auto(TypeName) _GNUC_CLEANUP(_AUTO_FUNC_NAME(TypeName)) TypeName
#define g_autofree _GNUC_CLEANUP(_cleanup_generic_autofree)

#endif


#endif // XSUSPENDER_MACROS_H
No changes.
@@ -0,0 +1,73 @@
#include "rule.h"

#include <glib.h>
#include <libwnck/libwnck.h>

#include "xsuspender.h"

Rule*
xsus_rule_copy (Rule *orig)
{
Rule *rule = g_memdup (orig, sizeof (Rule));

// Duplicate strings explicitly
rule->needle_wm_name = g_strdup (orig->needle_wm_name);
rule->needle_wm_class = g_strdup (orig->needle_wm_class);
rule->needle_wm_class_group = g_strdup (orig->needle_wm_class_group);

rule->exec_suspend = g_strdupv (orig->exec_suspend);
rule->exec_resume = g_strdupv (orig->exec_resume);

return rule;
}


void
xsus_rule_free (Rule *rule)
{
g_free (rule->needle_wm_class);
g_free (rule->needle_wm_class_group);
g_free (rule->needle_wm_name);

g_strfreev (rule->exec_suspend);
g_strfreev (rule->exec_resume);

g_free (rule);
}


static inline
gboolean
str_contains (const char *haystack,
const char *needle)
{
if (! needle)
return TRUE;
if (! haystack)
return FALSE;

return g_strstr_len (haystack, -1, needle) != NULL;
}


Rule*
xsus_window_get_rule (WnckWindow *window)
{
if (! WNCK_IS_WINDOW (window))
return NULL;

const char *wm_name = wnck_window_get_name (window),
*wm_class = wnck_window_get_class_instance_name (window),
*wm_class_group = wnck_window_get_class_group_name (window);

for (int i = 0; rules[i]; ++i) {
Rule *rule = rules[i];
// If all provided matching specifiers match
if (str_contains (wm_class_group, rule->needle_wm_class_group) &&
str_contains (wm_class, rule->needle_wm_class) &&
str_contains (wm_name, rule->needle_wm_name)) {
return rule;
}
}
return NULL;
}
@@ -0,0 +1,34 @@
#ifndef XSUSPENDER_RULE_H
#define XSUSPENDER_RULE_H

#include <glib.h>
#include <libwnck/libwnck.h>


// Configuration rule
typedef struct Rule {
char* needle_wm_name;
char* needle_wm_class;
char* needle_wm_class_group;

char** exec_suspend;
char** exec_resume;

char* subtree_pattern;

guint16 delay;
guint16 resume_every;
guint16 resume_for;

gboolean send_signals;
gboolean only_on_battery;
gboolean auto_on_battery;
} Rule;


Rule* xsus_rule_copy (Rule *rule);
void xsus_rule_free (Rule *rule);
Rule* xsus_window_get_rule (WnckWindow *window);


#endif // XSUSPENDER_RULE_H
@@ -0,0 +1,283 @@
#include "xsuspender.h"

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include <glib.h>
#include <libwnck/libwnck.h>

#include "config.h"
#include "entry.h"
#include "events.h"
#include "exec.h"
#include "macros.h"
#include "rule.h"


static
GMainLoop *loop;


gboolean
xsus_signal_stop (WindowEntry *entry)
{
Rule *rule = entry->rule;

// Run the windows' designated exec_suspend script, if any.
// If the subprocess fails, exit early without stopping the process.
if (xsus_exec_subprocess (rule->exec_suspend, entry) != 0) {
g_debug ("Subprocess failed; not stopping the process.");
return FALSE;
}

// Mark the process as suspended and kill it
g_debug ("kill -STOP %d", entry->pid);
suspended_entries = g_slist_prepend (suspended_entries, entry);

if (rule->send_signals) {
kill (entry->pid, SIGSTOP);
xsus_kill_subtree (entry->pid, SIGSTOP, rule->subtree_pattern);
}

return TRUE;
}


gboolean
xsus_signal_continue (WindowEntry *entry)
{
Rule *rule = entry->rule;

// Run the window's designated exec_resume script, if any.
// If the subprocess fails, continue the process anyway. Cause what were
// you going to do with a stuck process???
if (xsus_exec_subprocess (rule->exec_resume, entry) != 0)
g_debug ("Subprocess failed; resuming the process anyway.");

// Mark the process as not suspended and kill it
g_debug ("kill -CONT %d", entry->pid);
suspended_entries = g_slist_remove (suspended_entries, entry);

if (rule->send_signals) {
// Resume subprocesses before parent process to avoid the parent
// workers manager considering them "stuck" (cf. Firefox)
xsus_kill_subtree (entry->pid, SIGCONT, rule->subtree_pattern);

kill (entry->pid, SIGCONT);
}

// Free the entry now
xsus_window_entry_free (entry);

return TRUE;
}


void
xsus_window_entry_enqueue (WindowEntry *entry,
unsigned delay) {
// Mark the time of suspension
entry->suspend_timestamp = time (NULL) + delay;

// Schedule suspension
queued_entries = g_slist_prepend (queued_entries, entry);
}


void
xsus_window_resume (WnckWindow *window)
{
Rule *rule = xsus_window_get_rule (window);

// No matching configuration rule, window was not suspended
if (! rule)
return;

WindowEntry *entry;

// Remove the process from the pending queue
if ((entry = xsus_entry_find_for_window_rule (window, rule, queued_entries))) {
g_debug ("Removing window %#lx (%d) from suspension queue: %s",
wnck_window_get_xid (window),
wnck_window_get_pid (window),
wnck_window_get_name (window));
queued_entries = g_slist_remove (queued_entries, entry);
xsus_window_entry_free (entry);
return;
}

// Continue the process if it was actually stopped
if ((entry = xsus_entry_find_for_window_rule (window, rule, suspended_entries))) {
g_debug ("Resuming window %#lx (%d): %s",
wnck_window_get_xid (window),
wnck_window_get_pid (window),
wnck_window_get_name (window));
xsus_signal_continue (entry);
return;
}
}


void
xsus_window_suspend (WnckWindow *window)
{
Rule *rule = xsus_window_get_rule (window);

// No matching configuration rule, nothing to suspend
if (! rule)
return;

// Rule only applies on battery power and we are not
if (! is_battery_powered && rule->only_on_battery)
return;

// We shouldn't be having an entry for this window in the queues already ...
#ifndef NDEBUG
g_assert (! xsus_entry_find_for_window_rule (window, rule, suspended_entries));
g_assert (! xsus_entry_find_for_window_rule (window, rule, queued_entries));
#endif

// Schedule window suspension
g_debug ("Suspending window in %ds: %#lx (%d): %s",
rule->delay,
wnck_window_get_xid (window),
wnck_window_get_pid (window),
wnck_window_get_name (window));
WindowEntry *entry = xsus_window_entry_new (window, rule);
xsus_window_entry_enqueue (entry, rule->delay);
}


int
xsus_init ()
{
g_debug ("Initializing.");
IS_DEBUG = g_getenv ("G_MESSAGES_DEBUG") ? TRUE: FALSE;

// Nowadays common to have a single screen which combines several physical
// monitors. So it's ok to take the default. See:
// https://developer.gnome.org/libwnck/stable/WnckScreen.html#WnckScreen.description
// https://developer.gnome.org/gdk4/stable/GdkScreen.html#GdkScreen.description
if (! wnck_screen_get_default ())
g_critical ("Default screen is NULL. Not an X11 system? Too bad.");

// Parse the configuration files
rules = parse_config ();

is_battery_powered = FALSE;

// Init entry lists
suspended_entries = NULL;
queued_entries = NULL;

xsus_init_event_handlers ();

// Install exit signal handlers to exit gracefully
signal (SIGINT, xsus_exit);
signal (SIGTERM, xsus_exit);
signal (SIGABRT, xsus_exit);

// Don't call this function again, we're done
return FALSE;
}


void
xsus_exit ()
{
// Quit the main loop and thus, hopefully, exit
g_debug ("Exiting ...");
g_main_loop_quit (loop);
}


static inline
void
cleanup ()
{
wnck_shutdown ();

// Resume processes we have suspended; deallocate window entries
GSList *l = suspended_entries;
while (l) {
WindowEntry *entry = l->data;
l = l->next;
xsus_signal_continue (entry);
}

for (GSList *e = queued_entries; e; e = e->next)
xsus_window_entry_free (e->data);

g_slist_free (suspended_entries);
g_slist_free (queued_entries);

// Delete rules
for (int i = 0; rules[i]; ++i)
xsus_rule_free (rules[i]);
g_free (rules);
}


static
void
parse_args (int *argc,
char **argv[])
{
g_autoptr (GOptionContext) context = g_option_context_new (NULL);

g_option_context_set_help_enabled (context, FALSE);

g_option_context_set_summary (context,
"Automatically suspend inactive (unfocused) X11 windows (processes)\n"
"to save battery life. (v" PROJECT_VERSION ")");
g_option_context_set_description (context,
"The program looks for configuration in ~/.config/xsuspender.conf.\n"
"Example provided in " EXAMPLE_CONF ".\n"
"You can copy it over and adapt it to taste.\n"
"\n"
"To debug new configuration before it is put into use (recommended),\n"
"set environment variable G_MESSAGES_DEBUG=all, i.e.:\n"
"\n"
" G_MESSAGES_DEBUG=all xsuspender\n"
"\n"
"To daemonize the program, run it as:\n"
"\n"
" nohup xsuspender >/dev/null & disown\n"
"\n"
"or, better yet, ask your X session manager to run it for you.\n"
"\n"
"Read xsuspender(1) manual for more information.");

if (! g_option_context_parse (context, argc, argv, NULL)) {
printf ("%s", g_option_context_get_help (context, TRUE, NULL));
exit (EXIT_FAILURE);
}
}


int
main (int argc,
char *argv[])
{
// Parse command line arguments
gdk_init (&argc, &argv);
parse_args (&argc, &argv);

// Make g_critical() always exit
g_log_set_always_fatal (G_LOG_LEVEL_CRITICAL);

// Delay initialization until we're within the loop
g_timeout_add (1, xsus_init, NULL);

// Enter the main loop
loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop);

g_main_loop_unref (loop);
cleanup ();
g_debug ("Bye.");

return EXIT_SUCCESS;
}
@@ -0,0 +1,44 @@
#ifndef XSUSPENDER_XSUSPENDER_H
#define XSUSPENDER_XSUSPENDER_H

#include <glib.h>
#include <libwnck/libwnck.h>

#include "entry.h"
#include "rule.h"


#ifndef PROJECT_NAME
#warning "PROJECT_NAME undefined"
#define PROJECT_NAME "xsuspender"
#endif

#ifndef PROJECT_VERSION
#warning "PROJECT_VERSION undefined"
#define PROJECT_VERSION "0"
#endif

#ifndef EXAMPLE_CONF
#warning "EXAMPLE_CONF undefined"
#define EXAMPLE_CONF "/usr/share/doc/xsuspender/examples/xsuspender.conf"
#endif


gboolean IS_DEBUG;
gboolean is_battery_powered;

GSList *suspended_entries; // List of suspended WindowEntry
GSList *queued_entries;

Rule **rules; // Matching rules from config files


gboolean xsus_signal_stop (WindowEntry *entry);
gboolean xsus_signal_continue (WindowEntry *entry);
void xsus_window_entry_enqueue (WindowEntry *entry, unsigned delay);
void xsus_window_suspend (WnckWindow *window);
void xsus_window_resume (WnckWindow *window);
int xsus_init ();
void xsus_exit ();

#endif // XSUSPENDER_XSUSPENDER_H

This file was deleted.

Oops, something went wrong.