| @@ -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) | |||
| @@ -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 | |||
| @@ -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 | |||