diff --git a/README.md b/README.md index 531bdc2..f6ed925 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,6 @@ default values in the form `option-name=option-argument`. A common and effective use of this is to specify default `skip-options`, for instance skipping the `gamma` setting if using [`redshift`](https://github.com/jonls/redshift) as a daemon. To implement -the equivalent of `--skip-options gamma`, your `settings.ini` file should look like this: ``` @@ -238,6 +237,57 @@ notify-send -i display "Display profile" "$AUTORANDR_CURRENT_PROFILE" The one kink is that during `preswitch`, `AUTORANDR_CURRENT_PROFILE` is reporting the *upcoming* profile rather than the *current* one. +#### Exit status + +If a hook script exits with a non-zero exit status, a warning will be output, but the +operation continue. + +#### Canceling operation from within a hook script + +A hook script can send a `SIGUSR1` signal to its parent process to indicate that the +current operation should halt. The script is allowed to finish in any case. +Only scripts with names starting with `pre` support this feature. + +Bash example: + +```bash +#!/usr/bin/env bash + +should_continue() { + # No + return 1 +} + +if ! should_continue; then + # Send SIGUSR1 signal to parent process to cancel operation + kill -s USR1 "$PPID" + exit +fi + +# Continue ... +``` + +Python example: + +```python +#!/usr/bin/env python +import os +import sys +import signal + +def should_continue(): + # No + return False + +if __name__ == "__main__": + if not should_continue(): + # Send SIGUSR1 signal to parent process to cancel operation + os.kill(os.getppid(), signal.SIGUSR1) + sys.exit() + + # Continue ... +``` + ### Wildcard EDID matching The EDID strings in the `~/.config/autorandr/*/setup` files may contain an diff --git a/autorandr.py b/autorandr.py index 431069a..4940e24 100755 --- a/autorandr.py +++ b/autorandr.py @@ -24,6 +24,8 @@ from __future__ import print_function +from signal import signal, SIGUSR1, SIG_DFL + import binascii import copy import getopt @@ -61,6 +63,9 @@ except NameError: pass + +cancel_signal = SIGUSR1 + virtual_profiles = [ # (name, description, callback) ("off", "Disable all outputs", None), @@ -1179,10 +1184,7 @@ def exec_scripts(profile_path, script_name, meta_information=None): if script_name not in ran_scripts: script = os.path.join(folder, script_name) if os.access(script, os.X_OK | os.F_OK): - try: - all_ok &= subprocess.call(script, env=env) != 0 - except: - raise AutorandrException("Failed to execute user command: %s" % (script,)) + all_ok &= exec_one_script(script, script_name, env) != 0 ran_scripts.add(script_name) script_folder = os.path.join(folder, "%s.d" % script_name) @@ -1192,15 +1194,44 @@ def exec_scripts(profile_path, script_name, meta_information=None): if check_name not in ran_scripts: script = os.path.join(script_folder, file_name) if os.access(script, os.X_OK | os.F_OK): - try: - all_ok &= subprocess.call(script, env=env) != 0 - except: - raise AutorandrException("Failed to execute user command: %s" % (script,)) + all_ok &= exec_one_script(script, script_name, env) != 0 ran_scripts.add(check_name) return all_ok +def exec_one_script(script, script_name, env): + """"Run a userscript and return the exit code. + + If the script exits with a non-zero exit status, a warning is sent to stderr but the operation continues. + + If the script sends a SIGUSR1 signal to the parent, the parent process will + wait for the child process to exit, then halt with a zero status. + """ + def handle_cancel_signal(signum, frame): + if not script_name.startswith('pre'): + print("Script %s issued %s signal, but it is ignored for %s scripts." % ( + script, cancel_signal.name, script_name), file=sys.stderr) + return + + main_operation_name = script_name[3:] + + # Clean up, inform, exit + print("Script %s issued %s signal. Cancelling %s operation." % (script, cancel_signal.name, main_operation_name)) + sys.exit() + + try: + signal(cancel_signal, handle_cancel_signal) + return subprocess.check_call(script, env=env) + except subprocess.CalledProcessError as e: + print("Warning: Script %s returned exit code %d." % (script, e.returncode), file=sys.stderr) + except Exception as e: + raise AutorandrException("Failed to execute user command: %s" % (script,), original_exception=e) + finally: + # Revert to default handler + signal(cancel_signal, SIG_DFL) + + def dispatch_call_to_sessions(argv): """Invoke autorandr for each open local X11 session with the given options.