]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr.py
Output all non-error output to stdout instead of stderr
[deb_pkgs/autorandr.git] / autorandr.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # autorandr.py
5 # Copyright (c) 2015, Phillip Berndt
6 #
7 # Autorandr rewrite in Python
8 #
9 # This script aims to be fully compatible with the original autorandr.
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 from __future__ import print_function
26
27 import binascii
28 import copy
29 import getopt
30 import hashlib
31 import os
32 import posix
33 import pwd
34 import re
35 import subprocess
36 import sys
37 import shutil
38 import time
39
40 from collections import OrderedDict
41 from distutils.version import LooseVersion as Version
42 from functools import reduce
43 from itertools import chain
44
45 if sys.version_info.major == 2:
46     import ConfigParser as configparser
47 else:
48     import configparser
49
50 try:
51     input = raw_input
52 except NameError:
53     pass
54
55 virtual_profiles = [
56     # (name, description, callback)
57     ("off", "Disable all outputs", None),
58     ("common", "Clone all connected outputs at the largest common resolution", None),
59     ("clone-largest", "Clone all connected outputs with the largest resolution (scaled down if necessary)", None),
60     ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
61     ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
62 ]
63
64 help_text = """
65 Usage: autorandr [options]
66
67 -h, --help              get this small help
68 -c, --change            reload current setup
69 -d, --default <profile> make profile <profile> the default profile
70 -l, --load <profile>    load profile <profile>
71 -s, --save <profile>    save your current setup to profile <profile>
72 -r, --remove <profile>  remove profile <profile>
73 --batch                 run autorandr for all users with active X11 sessions
74 --config                dump your current xrandr setup
75 --debug                 enable verbose output
76 --dry-run               don't change anything, only print the xrandr commands
77 --fingerprint           fingerprint your current hardware setup
78 --force                 force (re)loading of a profile
79 --skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
80                         to skip both in detecting changes and applying a profile
81
82  If no suitable profile can be identified, the current configuration is kept.
83  To change this behaviour and switch to a fallback configuration, specify
84  --default <profile>.
85
86  autorandr supports a set of per-profile and global hooks. See the documentation
87  for details.
88
89  The following virtual configurations are available:
90 """.strip()
91
92
93 class AutorandrException(Exception):
94     def __init__(self, message, original_exception=None, report_bug=False):
95         self.message = message
96         self.report_bug = report_bug
97         if original_exception:
98             self.original_exception = original_exception
99             trace = sys.exc_info()[2]
100             while trace.tb_next:
101                 trace = trace.tb_next
102             self.line = trace.tb_lineno
103             self.file_name = trace.tb_frame.f_code.co_filename
104         else:
105             try:
106                 import inspect
107                 frame = inspect.currentframe().f_back
108                 self.line = frame.f_lineno
109                 self.file_name = frame.f_code.co_filename
110             except:
111                 self.line = None
112                 self.file_name = None
113             self.original_exception = None
114
115         if os.path.abspath(self.file_name) == os.path.abspath(sys.argv[0]):
116             self.file_name = None
117
118     def __str__(self):
119         retval = [self.message]
120         if self.line:
121             retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
122         if self.original_exception:
123             retval.append(":\n  ")
124             retval.append(str(self.original_exception).replace("\n", "\n  "))
125         if self.report_bug:
126             retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream:"
127                           "\nhttps://github.com/phillipberndt/autorandr/issues"
128                           "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
129         return "".join(retval)
130
131
132 class XrandrOutput(object):
133     "Represents an XRandR output"
134
135     # This regular expression is used to parse an output in `xrandr --verbose'
136     XRANDR_OUTPUT_REGEXP = """(?x)
137         ^(?P<output>[^ ]+)\s+                                                           # Line starts with output name
138         (?:                                                                             # Differentiate disconnected and connected
139             disconnected |                                                              # in first line
140             unknown\ connection |
141             (?P<connected>connected)
142         )
143         \s*
144         (?P<primary>primary\ )?                                                         # Might be primary screen
145         (?:\s*
146             (?P<width>[0-9]+)x(?P<height>[0-9]+)                                        # Resolution (might be overridden below!)
147             \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+                                       # Position
148             (?:\(0x[0-9a-fA-F]+\)\s+)?                                                  # XID
149             (?P<rotate>(?:normal|left|right|inverted))\s+                               # Rotation
150             (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)?                                       # Reflection
151         )?                                                                              # .. but only if the screen is in use.
152         (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
153         (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))?                 # Panning information
154         (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))?               # Tracking information
155         (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))?                            # Border information
156         (?:\s*(?:                                                                       # Properties of the output
157             Gamma: (?P<gamma>(?:inf|[0-9\.: e])+) |                                     # Gamma value
158             Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) |                           # Transformation matrix
159             EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) |                               # EDID of the output
160             (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)*                                 # Other properties
161         ))+
162         \s*
163         (?P<modes>(?:
164             (?P<mode_name>\S+).+?\*current.*\s+                                         # Interesting (current) resolution:
165              h:\s+width\s+(?P<mode_width>[0-9]+).+\s+                                   # Extract rate
166              v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
167             \S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s*                                     # Other resolutions
168         )*)
169     """
170
171     XRANDR_OUTPUT_MODES_REGEXP = """(?x)
172         (?P<name>\S+).+?(?P<preferred>\+preferred)?\s+
173          h:\s+width\s+(?P<width>[0-9]+).+\s+
174          v:\s+height\s+(?P<height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
175     """
176
177     XRANDR_13_DEFAULTS = {
178         "transform": "1,0,0,0,1,0,0,0,1",
179         "panning": "0x0",
180     }
181
182     XRANDR_12_DEFAULTS = {
183         "reflect": "normal",
184         "rotate": "normal",
185         "gamma": "1.0:1.0:1.0",
186     }
187
188     XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
189
190     EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
191
192     def __repr__(self):
193         return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
194
195     @property
196     def short_edid(self):
197         return ("%s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else ""
198
199     @property
200     def options_with_defaults(self):
201         "Return the options dictionary, augmented with the default values that weren't set"
202         if "off" in self.options:
203             return self.options
204         options = {}
205         if xrandr_version() >= Version("1.3"):
206             options.update(self.XRANDR_13_DEFAULTS)
207         if xrandr_version() >= Version("1.2"):
208             options.update(self.XRANDR_12_DEFAULTS)
209         options.update(self.options)
210         return {a: b for a, b in options.items() if a not in self.ignored_options}
211
212     @property
213     def filtered_options(self):
214         "Return a dictionary of options without ignored options"
215         return {a: b for a, b in self.options.items() if a not in self.ignored_options}
216
217     @property
218     def option_vector(self):
219         "Return the command line parameters for XRandR for this instance"
220         args = ["--output", self.output]
221         for option, arg in sorted(self.options_with_defaults.items()):
222             args.append("--%s" % option)
223             if arg:
224                 args.append(arg)
225         return args
226
227     @property
228     def option_string(self):
229         "Return the command line parameters in the configuration file format"
230         options = ["output %s" % self.output]
231         for option, arg in sorted(self.filtered_options.items()):
232             if arg:
233                 options.append("%s %s" % (option, arg))
234             else:
235                 options.append(option)
236         return "\n".join(options)
237
238     @property
239     def sort_key(self):
240         "Return a key to sort the outputs for xrandr invocation"
241         if not self.edid:
242             return -2
243         if "off" in self.options:
244             return -1
245         if "pos" in self.options:
246             x, y = map(float, self.options["pos"].split("x"))
247         else:
248             x, y = 0, 0
249         return x + 10000 * y
250
251     def __init__(self, output, edid, options):
252         "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
253         self.output = output
254         self.edid = edid
255         self.options = options
256         self.ignored_options = []
257         self.remove_default_option_values()
258
259     def set_ignored_options(self, options):
260         "Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
261         self.ignored_options = list(options)
262
263     def remove_default_option_values(self):
264         "Remove values from the options dictionary that are superflous"
265         if "off" in self.options and len(self.options.keys()) > 1:
266             self.options = {"off": None}
267             return
268         for option, default_value in self.XRANDR_DEFAULTS.items():
269             if option in self.options and self.options[option] == default_value:
270                 del self.options[option]
271
272     @classmethod
273     def from_xrandr_output(cls, xrandr_output):
274         """Instanciate an XrandrOutput from the output of `xrandr --verbose'
275
276         This method also returns a list of modes supported by the output.
277         """
278         try:
279             xrandr_output = xrandr_output.replace("\r\n", "\n")
280             match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
281         except:
282             raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.",
283                                      report_bug=True)
284         if not match_object:
285             debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
286             raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug,
287                                      report_bug=True)
288         remainder = xrandr_output[len(match_object.group(0)):]
289         if remainder:
290             raise AutorandrException("Parsing XRandR output failed, %d bytes left unmatched after "
291                                      "regular expression, starting at byte %d with ..'%s'." %
292                                      (len(remainder), len(match_object.group(0)), remainder[:10]),
293                                      report_bug=True)
294
295         match = match_object.groupdict()
296
297         modes = []
298         if match["modes"]:
299             modes = []
300             for mode_match in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]):
301                 if mode_match.group("name"):
302                     modes.append(mode_match.groupdict())
303             if not modes:
304                 raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
305
306         options = {}
307         if not match["connected"]:
308             edid = None
309         elif match["edid"]:
310             edid = "".join(match["edid"].strip().split())
311         else:
312             edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
313
314         if not match["width"]:
315             options["off"] = None
316         else:
317             if match["mode_name"]:
318                 options["mode"] = match["mode_name"]
319             elif match["mode_width"]:
320                 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
321             else:
322                 if match["rotate"] not in ("left", "right"):
323                     options["mode"] = "%sx%s" % (match["width"], match["height"])
324                 else:
325                     options["mode"] = "%sx%s" % (match["height"], match["width"])
326             options["rotate"] = match["rotate"]
327             if match["primary"]:
328                 options["primary"] = None
329             if match["reflect"] == "X":
330                 options["reflect"] = "x"
331             elif match["reflect"] == "Y":
332                 options["reflect"] = "y"
333             elif match["reflect"] == "X and Y":
334                 options["reflect"] = "xy"
335             options["pos"] = "%sx%s" % (match["x"], match["y"])
336             if match["panning"]:
337                 panning = [match["panning"]]
338                 if match["tracking"]:
339                     panning += ["/", match["tracking"]]
340                     if match["border"]:
341                         panning += ["/", match["border"]]
342                 options["panning"] = "".join(panning)
343             if match["transform"]:
344                 transformation = ",".join(match["transform"].strip().split())
345                 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
346                     options["transform"] = transformation
347                     if not match["mode_name"]:
348                         # TODO We'd need to apply the reverse transformation here. Let's see if someone complains,
349                         # I doubt that this special case is actually required.
350                         print("Warning: Output %s has a transformation applied. Could not determine correct mode! "
351                               "Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
352             if match["gamma"]:
353                 gamma = match["gamma"].strip()
354                 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
355                 # Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
356                 # so we approximate by 1e-10.
357                 gamma = ":".join([str(max(1e-10, round(1. / float(x), 3))) for x in gamma.split(":")])
358                 options["gamma"] = gamma
359             if match["rate"]:
360                 options["rate"] = match["rate"]
361
362         return XrandrOutput(match["output"], edid, options), modes
363
364     @classmethod
365     def from_config_file(cls, edid_map, configuration):
366         "Instanciate an XrandrOutput from the contents of a configuration file"
367         options = {}
368         for line in configuration.split("\n"):
369             if line:
370                 line = line.split(None, 1)
371                 if line and line[0].startswith("#"):
372                     continue
373                 options[line[0]] = line[1] if len(line) > 1 else None
374
375         edid = None
376
377         if options["output"] in edid_map:
378             edid = edid_map[options["output"]]
379         else:
380             # This fuzzy matching is for legacy autorandr that used sysfs output names
381             fuzzy_edid_map = [re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys()]
382             fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
383             if fuzzy_output in fuzzy_edid_map:
384                 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
385             elif "off" not in options:
386                 raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' "
387                                          "is not off in config file." % (options["output"], options["output"]))
388         output = options["output"]
389         del options["output"]
390
391         return XrandrOutput(output, edid, options)
392
393     def edid_equals(self, other):
394         "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
395         if self.edid and other.edid:
396             if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
397                 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
398             if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
399                 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
400         return self.edid == other.edid
401
402     def __ne__(self, other):
403         return not (self == other)
404
405     def __eq__(self, other):
406         return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
407
408     def verbose_diff(self, other):
409         "Compare to another XrandrOutput and return a list of human readable differences"
410         diffs = []
411         if not self.edid_equals(other):
412             diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
413         if self.output != other.output:
414             diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
415         if "off" in self.options and "off" not in other.options:
416             diffs.append("The output is disabled currently, but active in the new configuration")
417         elif "off" in other.options and "off" not in self.options:
418             diffs.append("The output is currently enabled, but inactive in the new configuration")
419         else:
420             for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
421                 if name not in other.options:
422                     diffs.append("Option --%s %sis not present in the new configuration" %
423                                  (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
424                 elif name not in self.options:
425                     diffs.append("Option --%s (`%s' in the new configuration) is not present currently" %
426                                  (name, other.options[name]))
427                 elif self.options[name] != other.options[name]:
428                     diffs.append("Option --%s %sis `%s' in the new configuration" %
429                                  (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
430         return diffs
431
432
433 def xrandr_version():
434     "Return the version of XRandR that this system uses"
435     if getattr(xrandr_version, "version", False) is False:
436         version_string = os.popen("xrandr -v").read()
437         try:
438             version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
439             xrandr_version.version = Version(version)
440         except AttributeError:
441             xrandr_version.version = Version("1.3.0")
442
443     return xrandr_version.version
444
445
446 def debug_regexp(pattern, string):
447     "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
448     try:
449         import regex
450         bounds = (0, len(string))
451         while bounds[0] != bounds[1]:
452             half = int((bounds[0] + bounds[1]) / 2)
453             if half == bounds[0]:
454                 break
455             bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
456         partial_length = bounds[0]
457         return ("Regular expression matched until position %d, ..'%s', and did not match from '%s'.." %
458                 (partial_length, string[max(0, partial_length - 20):partial_length],
459                  string[partial_length:partial_length + 10]))
460     except ImportError:
461         pass
462     return "Debug information would be available if the `regex' module was installed."
463
464
465 def parse_xrandr_output():
466     "Parse the output of `xrandr --verbose' into a list of outputs"
467     xrandr_output = os.popen("xrandr -q --verbose").read()
468     if not xrandr_output:
469         raise AutorandrException("Failed to run xrandr")
470
471     # We are not interested in screens
472     xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
473
474     # Split at output boundaries and instanciate an XrandrOutput per output
475     split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
476     if len(split_xrandr_output) < 2:
477         raise AutorandrException("No output boundaries found", report_bug=True)
478     outputs = OrderedDict()
479     modes = OrderedDict()
480     for i in range(1, len(split_xrandr_output), 2):
481         output_name = split_xrandr_output[i].split()[0]
482         output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i + 2]))
483         outputs[output_name] = output
484         if output_modes:
485             modes[output_name] = output_modes
486
487     return outputs, modes
488
489
490 def load_profiles(profile_path):
491     "Load the stored profiles"
492
493     profiles = {}
494     for profile in os.listdir(profile_path):
495         config_name = os.path.join(profile_path, profile, "config")
496         setup_name = os.path.join(profile_path, profile, "setup")
497         if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
498             continue
499
500         edids = dict([x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#"])
501
502         config = {}
503         buffer = []
504         for line in chain(open(config_name).readlines(), ["output"]):
505             if line[:6] == "output" and buffer:
506                 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
507                 buffer = [line]
508             else:
509                 buffer.append(line)
510
511         for output_name in list(config.keys()):
512             if config[output_name].edid is None:
513                 del config[output_name]
514
515         profiles[profile] = {
516             "config": config,
517             "path": os.path.join(profile_path, profile),
518             "config-mtime": os.stat(config_name).st_mtime,
519         }
520
521     return profiles
522
523
524 def get_symlinks(profile_path):
525     "Load all symlinks from a directory"
526
527     symlinks = {}
528     for link in os.listdir(profile_path):
529         file_name = os.path.join(profile_path, link)
530         if os.path.islink(file_name):
531             symlinks[link] = os.readlink(file_name)
532
533     return symlinks
534
535
536 def find_profiles(current_config, profiles):
537     "Find profiles matching the currently connected outputs"
538     detected_profiles = []
539     for profile_name, profile in profiles.items():
540         config = profile["config"]
541         matches = True
542         for name, output in config.items():
543             if not output.edid:
544                 continue
545             if name not in current_config or not output.edid_equals(current_config[name]):
546                 matches = False
547                 break
548         if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
549             continue
550         if matches:
551             detected_profiles.append(profile_name)
552     return detected_profiles
553
554
555 def profile_blocked(profile_path, meta_information=None):
556     """Check if a profile is blocked.
557
558     meta_information is expected to be an dictionary. It will be passed to the block scripts
559     in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
560     """
561     return not exec_scripts(profile_path, "block", meta_information)
562
563
564 def output_configuration(configuration, config):
565     "Write a configuration file"
566     outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
567     for output in outputs:
568         print(configuration[output].option_string, file=config)
569
570
571 def output_setup(configuration, setup):
572     "Write a setup (fingerprint) file"
573     outputs = sorted(configuration.keys())
574     for output in outputs:
575         if configuration[output].edid:
576             print(output, configuration[output].edid, file=setup)
577
578
579 def save_configuration(profile_path, configuration):
580     "Save a configuration into a profile"
581     if not os.path.isdir(profile_path):
582         os.makedirs(profile_path)
583     with open(os.path.join(profile_path, "config"), "w") as config:
584         output_configuration(configuration, config)
585     with open(os.path.join(profile_path, "setup"), "w") as setup:
586         output_setup(configuration, setup)
587
588
589 def update_mtime(filename):
590     "Update a file's mtime"
591     try:
592         os.utime(filename, None)
593         return True
594     except:
595         return False
596
597
598 def call_and_retry(*args, **kwargs):
599     """Wrapper around subprocess.call that retries failed calls.
600
601     This function calls subprocess.call and on non-zero exit states,
602     waits a second and then retries once. This mitigates #47,
603     a timing issue with some drivers.
604     """
605     if "dry_run" in kwargs:
606         dry_run = kwargs["dry_run"]
607         del kwargs["dry_run"]
608     else:
609         dry_run = False
610     kwargs_redirected = dict(kwargs)
611     if not dry_run:
612         if hasattr(subprocess, "DEVNULL"):
613             kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
614         else:
615             kwargs_redirected["stdout"] = open(os.devnull, "w")
616         kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
617     retval = subprocess.call(*args, **kwargs_redirected)
618     if retval != 0:
619         time.sleep(1)
620         retval = subprocess.call(*args, **kwargs)
621     return retval
622
623
624 def apply_configuration(new_configuration, current_configuration, dry_run=False):
625     "Apply a configuration"
626     outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
627     if dry_run:
628         base_argv = ["echo", "xrandr"]
629     else:
630         base_argv = ["xrandr"]
631
632     # There are several xrandr / driver bugs we need to take care of here:
633     # - We cannot enable more than two screens at the same time
634     #   See https://github.com/phillipberndt/autorandr/pull/6
635     #   and commits f4cce4d and 8429886.
636     # - We cannot disable all screens
637     #   See https://github.com/phillipberndt/autorandr/pull/20
638     # - We should disable screens before enabling others, because there's
639     #   a limit on the number of enabled screens
640     # - We must make sure that the screen at 0x0 is activated first,
641     #   or the other (first) screen to be activated would be moved there.
642     # - If an active screen already has a transformation and remains active,
643     #   the xrandr call fails with an invalid RRSetScreenSize parameter error.
644     #   Update the configuration in 3 passes in that case.  (On Haswell graphics,
645     #   at least.)
646     # - Some implementations can not handle --transform at all, so avoid it unless
647     #   necessary. (See https://github.com/phillipberndt/autorandr/issues/37)
648     # - Some implementations can not handle --panning without specifying --fb
649     #   explicitly, so avoid it unless necessary.
650     #   (See https://github.com/phillipberndt/autorandr/issues/72)
651
652     auxiliary_changes_pre = []
653     disable_outputs = []
654     enable_outputs = []
655     remain_active_count = 0
656     for output in outputs:
657         if not new_configuration[output].edid or "off" in new_configuration[output].options:
658             disable_outputs.append(new_configuration[output].option_vector)
659         else:
660             if "off" not in current_configuration[output].options:
661                 remain_active_count += 1
662
663             option_vector = new_configuration[output].option_vector
664             if xrandr_version() >= Version("1.3.0"):
665                 for option, off_value in (("transform", "none"), ("panning", "0x0")):
666                     if option in current_configuration[output].options:
667                         auxiliary_changes_pre.append(["--output", output, "--%s" % option, off_value])
668                     else:
669                         try:
670                             option_index = option_vector.index("--%s" % option)
671                             if option_vector[option_index + 1] == XrandrOutput.XRANDR_DEFAULTS[option]:
672                                 option_vector = option_vector[:option_index] + option_vector[option_index + 2:]
673                         except ValueError:
674                             pass
675
676             enable_outputs.append(option_vector)
677
678     # Perform pe-change auxiliary changes
679     if auxiliary_changes_pre:
680         argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
681         if call_and_retry(argv, dry_run=dry_run) != 0:
682             raise AutorandrException("Command failed: %s" % " ".join(argv))
683
684     # Disable unused outputs, but make sure that there always is at least one active screen
685     disable_keep = 0 if remain_active_count else 1
686     if len(disable_outputs) > disable_keep:
687         argv = base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))
688         if call_and_retry(argv, dry_run=dry_run) != 0:
689             # Disabling the outputs failed. Retry with the next command:
690             # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
691             # This does not occur if simultaneously the primary screen is reset.
692             pass
693         else:
694             disable_outputs = disable_outputs[-1:] if disable_keep else []
695
696     # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
697     # disable the last two screens. This is a problem, so if this would happen, instead disable only
698     # one screen in the first call below.
699     if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
700         # In the context of a xrandr call that changes the display state, `--query' should do nothing
701         disable_outputs.insert(0, ['--query'])
702
703     # Enable the remaining outputs in pairs of two operations
704     operations = disable_outputs + enable_outputs
705     for index in range(0, len(operations), 2):
706         argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
707         if call_and_retry(argv, dry_run=dry_run) != 0:
708             raise AutorandrException("Command failed: %s" % " ".join(argv))
709
710
711 def is_equal_configuration(source_configuration, target_configuration):
712     "Check if all outputs from target are already configured correctly in source"
713     for output in target_configuration.keys():
714         if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
715             return False
716     return True
717
718
719 def add_unused_outputs(source_configuration, target_configuration):
720     "Add outputs that are missing in target to target, in 'off' state"
721     for output_name, output in source_configuration.items():
722         if output_name not in target_configuration:
723             target_configuration[output_name] = XrandrOutput(output_name, output.edid, {"off": None})
724
725
726 def remove_irrelevant_outputs(source_configuration, target_configuration):
727     "Remove outputs from target that ought to be 'off' and already are"
728     for output_name, output in source_configuration.items():
729         if "off" in output.options:
730             if output_name in target_configuration:
731                 if "off" in target_configuration[output_name].options:
732                     del target_configuration[output_name]
733
734
735 def generate_virtual_profile(configuration, modes, profile_name):
736     "Generate one of the virtual profiles"
737     configuration = copy.deepcopy(configuration)
738     if profile_name == "common":
739         mode_sets = []
740         for output, output_modes in modes.items():
741             mode_set = set()
742             if configuration[output].edid:
743                 for mode in output_modes:
744                     mode_set.add((mode["width"], mode["height"]))
745             mode_sets.append(mode_set)
746         common_resolution = reduce(lambda a, b: a & b, mode_sets[1:], mode_sets[0])
747         common_resolution = sorted(common_resolution, key=lambda a: int(a[0]) * int(a[1]))
748         if common_resolution:
749             for output in configuration:
750                 configuration[output].options = {}
751                 if output in modes and configuration[output].edid:
752                     modes_sorted = sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1)
753                     modes_filtered = [x for x in modes_sorted if (x["width"], x["height"]) == common_resolution[-1]]
754                     mode = modes_filtered[0]
755                     configuration[output].options["mode"] = mode['name']
756                     configuration[output].options["pos"] = "0x0"
757                 else:
758                     configuration[output].options["off"] = None
759     elif profile_name in ("horizontal", "vertical"):
760         shift = 0
761         if profile_name == "horizontal":
762             shift_index = "width"
763             pos_specifier = "%sx0"
764         else:
765             shift_index = "height"
766             pos_specifier = "0x%s"
767
768         for output in configuration:
769             configuration[output].options = {}
770             if output in modes and configuration[output].edid:
771                 def key(a):
772                     score = int(a["width"]) * int(a["height"])
773                     if a["preferred"]:
774                         score += 10**6
775                     return score
776                 output_modes = sorted(modes[output], key=key)
777                 mode = output_modes[-1]
778                 configuration[output].options["mode"] = mode["name"]
779                 configuration[output].options["rate"] = mode["rate"]
780                 configuration[output].options["pos"] = pos_specifier % shift
781                 shift += int(mode[shift_index])
782             else:
783                 configuration[output].options["off"] = None
784     elif profile_name == "clone-largest":
785         modes_unsorted = [output_modes[0] for output, output_modes in modes.items()]
786         modes_sorted = sorted(modes_unsorted, key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)
787         biggest_resolution = modes_sorted[0]
788         for output in configuration:
789             configuration[output].options = {}
790             if output in modes and configuration[output].edid:
791                 def key(a):
792                     score = int(a["width"]) * int(a["height"])
793                     if a["preferred"]:
794                         score += 10**6
795                     return score
796                 output_modes = sorted(modes[output], key=key)
797                 mode = output_modes[-1]
798                 configuration[output].options["mode"] = mode["name"]
799                 configuration[output].options["rate"] = mode["rate"]
800                 configuration[output].options["pos"] = "0x0"
801                 scale = max(float(biggest_resolution["width"]) / float(mode["width"]),
802                             float(biggest_resolution["height"]) / float(mode["height"]))
803                 mov_x = (float(mode["width"]) * scale - float(biggest_resolution["width"])) / -2
804                 mov_y = (float(mode["height"]) * scale - float(biggest_resolution["height"])) / -2
805                 configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
806             else:
807                 configuration[output].options["off"] = None
808     elif profile_name == "off":
809         for output in configuration:
810             for key in list(configuration[output].options.keys()):
811                 del configuration[output].options[key]
812             configuration[output].options["off"] = None
813     return configuration
814
815
816 def print_profile_differences(one, another):
817     "Print the differences between two profiles for debugging"
818     if one == another:
819         return
820     print("| Differences between the two profiles:")
821     for output in set(chain.from_iterable((one.keys(), another.keys()))):
822         if output not in one:
823             if "off" not in another[output].options:
824                 print("| Output `%s' is missing from the active configuration" % output)
825         elif output not in another:
826             if "off" not in one[output].options:
827                 print("| Output `%s' is missing from the new configuration" % output)
828         else:
829             for line in one[output].verbose_diff(another[output]):
830                 print("| [Output %s] %s" % (output, line))
831     print("\\-")
832
833
834 def exit_help():
835     "Print help and exit"
836     print(help_text)
837     for profile in virtual_profiles:
838         name, description = profile[:2]
839         description = [description]
840         max_width = 78 - 18
841         while len(description[0]) > max_width + 1:
842             left_over = description[0][max_width:]
843             description[0] = description[0][:max_width] + "-"
844             description.insert(1, "  %-15s %s" % ("", left_over))
845         description = "\n".join(description)
846         print("  %-15s %s" % (name, description))
847     sys.exit(0)
848
849
850 def exec_scripts(profile_path, script_name, meta_information=None):
851     """"Run userscripts
852
853     This will run all executables from the profile folder, and global per-user
854     and system-wide configuration folders, named script_name or residing in
855     subdirectories named script_name.d.
856
857     If profile_path is None, only global scripts will be invoked.
858
859     meta_information is expected to be an dictionary. It will be passed to the block scripts
860     in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
861
862     Returns True unless any of the scripts exited with non-zero exit status.
863     """
864     all_ok = True
865     env = os.environ.copy()
866     if meta_information:
867         for key, value in meta_information.items():
868             env["AUTORANDR_{}".format(key.upper())] = str(value)
869
870     # If there are multiple candidates, the XDG spec tells to only use the first one.
871     ran_scripts = set()
872
873     user_profile_path = os.path.expanduser("~/.autorandr")
874     if not os.path.isdir(user_profile_path):
875         user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
876
877     candidate_directories = [user_profile_path]
878     for config_dir in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"):
879         candidate_directories.append(os.path.join(config_dir, "autorandr"))
880     if profile_path:
881         candidate_directories.append(profile_path)
882
883     for folder in candidate_directories:
884         if script_name not in ran_scripts:
885             script = os.path.join(folder, script_name)
886             if os.access(script, os.X_OK | os.F_OK):
887                 try:
888                     all_ok &= subprocess.call(script, env=env) != 0
889                 except:
890                     raise AutorandrException("Failed to execute user command: %s" % (script,))
891                 ran_scripts.add(script_name)
892
893         script_folder = os.path.join(folder, "%s.d" % script_name)
894         if os.access(script_folder, os.R_OK | os.X_OK) and os.path.isdir(script_folder):
895             for file_name in os.listdir(script_folder):
896                 check_name = "d/%s" % (file_name,)
897                 if check_name not in ran_scripts:
898                     script = os.path.join(script_folder, file_name)
899                     if os.access(script, os.X_OK | os.F_OK):
900                         try:
901                             all_ok &= subprocess.call(script, env=env) != 0
902                         except:
903                             raise AutorandrException("Failed to execute user command: %s" % (script,))
904                         ran_scripts.add(check_name)
905
906     return all_ok
907
908
909 def dispatch_call_to_sessions(argv):
910     """Invoke autorandr for each open local X11 session with the given options.
911
912     The function iterates over all processes not owned by root and checks
913     whether they have DISPLAY and XAUTHORITY variables set. It strips the
914     screen from any variable it finds (i.e. :0.0 becomes :0) and checks whether
915     this display has been handled already. If it has not, it forks, changes
916     uid/gid to the user owning the process, reuses the process's environment
917     and runs autorandr with the parameters from argv.
918
919     This function requires root permissions. It only works for X11 servers that
920     have at least one non-root process running. It is susceptible for attacks
921     where one user runs a process with another user's DISPLAY variable - in
922     this case, it might happen that autorandr is invoked for the other user,
923     which won't work. Since no other harm than prevention of automated
924     execution of autorandr can be done this way, the assumption is that in this
925     situation, the local administrator will handle the situation."""
926
927     X11_displays_done = set()
928
929     autorandr_binary = os.path.abspath(argv[0])
930     backup_candidates = {}
931
932     def fork_child_autorandr(pwent, process_environ):
933         print("Running autorandr as %s for display %s" % (pwent.pw_name, process_environ["DISPLAY"]))
934         child_pid = os.fork()
935         if child_pid == 0:
936             # This will throw an exception if any of the privilege changes fails,
937             # so it should be safe. Also, note that since the environment
938             # is taken from a process owned by the user, reusing it should
939             # not leak any information.
940             os.setgroups([])
941             os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
942             os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
943             os.chdir(pwent.pw_dir)
944             os.environ.clear()
945             os.environ.update(process_environ)
946             os.execl(autorandr_binary, autorandr_binary, *argv[1:])
947             os.exit(1)
948         os.waitpid(child_pid, 0)
949
950     for directory in os.listdir("/proc"):
951         directory = os.path.join("/proc/", directory)
952         if not os.path.isdir(directory):
953             continue
954         environ_file = os.path.join(directory, "environ")
955         if not os.path.isfile(environ_file):
956             continue
957         uid = os.stat(environ_file).st_uid
958
959         # The following line assumes that user accounts start at 1000 and that
960         # no one works using the root or another system account. This is rather
961         # restrictive, but de facto default. Alternatives would be to use the
962         # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
963         # but effectively, both values aren't binding in any way.
964         # If this breaks your use case, please file a bug on Github.
965         if uid < 1000:
966             continue
967
968         process_environ = {}
969         for environ_entry in open(environ_file).read().split("\0"):
970             name, sep, value = environ_entry.partition("=")
971             if name and sep:
972                 if name == "DISPLAY" and "." in value:
973                     value = value[:value.find(".")]
974                 process_environ[name] = value
975
976         if "DISPLAY" not in process_environ:
977             # Cannot work with this environment, skip.
978             continue
979
980         # To allow scripts to detect batch invocation (especially useful for predetect)
981         process_environ["AUTORANDR_BATCH_PID"] = str(os.getpid())
982         process_environ["UID"] = str(uid)
983
984         display = process_environ["DISPLAY"]
985
986         if "XAUTHORITY" not in process_environ:
987             # It's very likely that we cannot work with this environment either,
988             # but keep it as a backup just in case we don't find anything else.
989             backup_candidates[display] = process_environ
990             continue
991
992         if display not in X11_displays_done:
993             try:
994                 pwent = pwd.getpwuid(uid)
995             except KeyError:
996                 # User has no pwd entry
997                 continue
998
999             fork_child_autorandr(pwent, process_environ)
1000             X11_displays_done.add(display)
1001
1002     # Run autorandr for any users/displays which didn't have a process with
1003     # XAUTHORITY set.
1004     for display, process_environ in backup_candidates.items():
1005         if display not in X11_displays_done:
1006             try:
1007                 pwent = pwd.getpwuid(int(process_environ["UID"]))
1008             except KeyError:
1009                 # User has no pwd entry
1010                 continue
1011
1012             fork_child_autorandr(pwent, process_environ)
1013             X11_displays_done.add(display)
1014
1015
1016 def read_config(options, directory):
1017     """Parse a configuration config.ini from directory and merge it into
1018     the options dictionary"""
1019     config = configparser.ConfigParser()
1020     config.read(os.path.join(directory, "settings.ini"))
1021     if config.has_section("config"):
1022         for key, value in config.items("config"):
1023             options.setdefault("--%s" % key, value)
1024
1025 def main(argv):
1026     try:
1027         opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
1028                                    ["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
1029                                     "force", "fingerprint", "config", "debug", "skip-options=", "help"])
1030     except getopt.GetoptError as e:
1031         print("Failed to parse options: {0}.\n"
1032               "Use --help to get usage information.".format(str(e)),
1033               file=sys.stderr)
1034         sys.exit(posix.EX_USAGE)
1035
1036     options = dict(opts)
1037
1038     if "-h" in options or "--help" in options:
1039         exit_help()
1040
1041     # Batch mode
1042     if "--batch" in options:
1043         if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
1044             dispatch_call_to_sessions([x for x in argv if x != "--batch"])
1045         else:
1046             print("--batch mode can only be used by root and if $DISPLAY is unset")
1047         return
1048     if "AUTORANDR_BATCH_PID" in os.environ:
1049         user = pwd.getpwuid(os.getuid())
1050         user = user.pw_name if user else "#%d" % os.getuid()
1051         print("autorandr running as user %s (started from batch instance)" % user)
1052
1053     profiles = {}
1054     profile_symlinks = {}
1055     try:
1056         # Load profiles from each XDG config directory
1057         # The XDG spec says that earlier entries should take precedence, so reverse the order
1058         for directory in reversed(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")):
1059             system_profile_path = os.path.join(directory, "autorandr")
1060             if os.path.isdir(system_profile_path):
1061                 profiles.update(load_profiles(system_profile_path))
1062                 profile_symlinks.update(get_symlinks(system_profile_path))
1063                 read_config(options, system_profile_path)
1064         # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
1065         # profile_path is also used later on to store configurations
1066         profile_path = os.path.expanduser("~/.autorandr")
1067         if not os.path.isdir(profile_path):
1068             # Elsewise, follow the XDG specification
1069             profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
1070         if os.path.isdir(profile_path):
1071             profiles.update(load_profiles(profile_path))
1072             profile_symlinks.update(get_symlinks(profile_path))
1073             read_config(options, profile_path)
1074         # Sort by descending mtime
1075         profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
1076     except Exception as e:
1077         raise AutorandrException("Failed to load profiles", e)
1078
1079     profile_symlinks = {k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles}
1080
1081     exec_scripts(None, "predetect")
1082     config, modes = parse_xrandr_output()
1083
1084     if "--fingerprint" in options:
1085         output_setup(config, sys.stdout)
1086         sys.exit(0)
1087
1088     if "--config" in options:
1089         output_configuration(config, sys.stdout)
1090         sys.exit(0)
1091
1092     if "--skip-options" in options:
1093         skip_options = [y[2:] if y[:2] == "--" else y for y in (x.strip() for x in options["--skip-options"].split(","))]
1094         for profile in profiles.values():
1095             for output in profile["config"].values():
1096                 output.set_ignored_options(skip_options)
1097         for output in config.values():
1098             output.set_ignored_options(skip_options)
1099
1100     if "-s" in options:
1101         options["--save"] = options["-s"]
1102     if "--save" in options:
1103         if options["--save"] in (x[0] for x in virtual_profiles):
1104             raise AutorandrException("Cannot save current configuration as profile '%s':\n"
1105                                      "This configuration name is a reserved virtual configuration." % options["--save"])
1106         try:
1107             profile_folder = os.path.join(profile_path, options["--save"])
1108             save_configuration(profile_folder, config)
1109             exec_scripts(profile_folder, "postsave", {"CURRENT_PROFILE": options["--save"], "PROFILE_FOLDER": profile_folder})
1110         except Exception as e:
1111             raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
1112         print("Saved current configuration as profile '%s'" % options["--save"])
1113         sys.exit(0)
1114
1115     if "-r" in options:
1116         options["--remove"] = options["-r"]
1117     if "--remove" in options:
1118         if options["--remove"] in (x[0] for x in virtual_profiles):
1119             raise AutorandrException("Cannot remove profile '%s':\n"
1120                                      "This configuration name is a reserved virtual configuration." % options["--remove"])
1121         if options["--remove"] not in profiles.keys():
1122             raise AutorandrException("Cannot remove profile '%s':\n"
1123                                      "This profile does not exist." % options["--remove"])
1124         try:
1125             remove = True
1126             profile_folder = os.path.join(profile_path, options["--remove"])
1127             profile_dirlist = os.listdir(profile_folder)
1128             profile_dirlist.remove("config")
1129             profile_dirlist.remove("setup")
1130             if profile_dirlist:
1131                 print("Profile folder '%s' contains the following additional files:\n"
1132                       "---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
1133                 response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
1134                 if response != "yes":
1135                     remove = False
1136             if remove is True:
1137                 shutil.rmtree(profile_folder)
1138                 print("Removed profile '%s'" % options["--remove"])
1139             else:
1140                 print("Profile '%s' was not removed" % options["--remove"])
1141         except Exception as e:
1142             raise AutorandrException("Failed to remove profile '%s'" % (options["--remove"],), e)
1143         sys.exit(0)
1144
1145     detected_profiles = find_profiles(config, profiles)
1146     load_profile = False
1147
1148     if "-l" in options:
1149         options["--load"] = options["-l"]
1150     if "--load" in options:
1151         load_profile = options["--load"]
1152     elif len(args) == 1:
1153         load_profile = args[0]
1154     else:
1155         # Find the active profile(s) first, for the block script (See #42)
1156         current_profiles = []
1157         for profile_name in profiles.keys():
1158             configs_are_equal = is_equal_configuration(config, profiles[profile_name]["config"])
1159             if configs_are_equal:
1160                 current_profiles.append(profile_name)
1161         block_script_metadata = {
1162             "CURRENT_PROFILE": "".join(current_profiles[:1]),
1163             "CURRENT_PROFILES": ":".join(current_profiles)
1164         }
1165
1166         for profile_name in profiles.keys():
1167             if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
1168                 print("%s (blocked)" % profile_name)
1169                 continue
1170             props = []
1171             if profile_name in detected_profiles:
1172                 props.append("(detected)")
1173                 if ("-c" in options or "--change" in options) and not load_profile:
1174                     load_profile = profile_name
1175             if profile_name in current_profiles:
1176                 props.append("(current)")
1177             print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
1178             if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
1179                 print_profile_differences(config, profiles[profile_name]["config"])
1180
1181     if "-d" in options:
1182         options["--default"] = options["-d"]
1183     if not load_profile and "--default" in options:
1184         load_profile = options["--default"]
1185
1186     if load_profile:
1187         if load_profile in profile_symlinks:
1188             if "--debug" in options:
1189                 print("'%s' symlinked to '%s'" % (load_profile, profile_symlinks[load_profile]))
1190             load_profile = profile_symlinks[load_profile]
1191
1192         if load_profile in (x[0] for x in virtual_profiles):
1193             load_config = generate_virtual_profile(config, modes, load_profile)
1194             scripts_path = os.path.join(profile_path, load_profile)
1195         else:
1196             try:
1197                 profile = profiles[load_profile]
1198                 load_config = profile["config"]
1199                 scripts_path = profile["path"]
1200             except KeyError:
1201                 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
1202             if load_profile in detected_profiles and detected_profiles[0] != load_profile:
1203                 update_mtime(os.path.join(scripts_path, "config"))
1204         add_unused_outputs(config, load_config)
1205         if load_config == dict(config) and "-f" not in options and "--force" not in options:
1206             print("Config already loaded", file=sys.stderr)
1207             sys.exit(0)
1208         if "--debug" in options and load_config != dict(config):
1209             print("Loading profile '%s'" % load_profile)
1210             print_profile_differences(config, load_config)
1211
1212         remove_irrelevant_outputs(config, load_config)
1213
1214         try:
1215             if "--dry-run" in options:
1216                 apply_configuration(load_config, config, True)
1217             else:
1218                 script_metadata = {
1219                     "CURRENT_PROFILE": load_profile,
1220                     "PROFILE_FOLDER": scripts_path,
1221                 }
1222                 exec_scripts(scripts_path, "preswitch", script_metadata)
1223                 if "--debug" in options:
1224                     print("Going to run:")
1225                     apply_configuration(load_config, config, True)
1226                 apply_configuration(load_config, config, False)
1227                 exec_scripts(scripts_path, "postswitch", script_metadata)
1228         except AutorandrException as e:
1229             raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, e.report_bug)
1230         except Exception as e:
1231             raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
1232
1233         if "--dry-run" not in options and "--debug" in options:
1234             new_config, _ = parse_xrandr_output()
1235             if not is_equal_configuration(new_config, load_config):
1236                 print("The configuration change did not go as expected:")
1237                 print_profile_differences(new_config, load_config)
1238
1239     sys.exit(0)
1240
1241
1242 def exception_handled_main(argv=sys.argv):
1243     try:
1244         main(sys.argv)
1245     except AutorandrException as e:
1246         print(e, file=sys.stderr)
1247         sys.exit(1)
1248     except Exception as e:
1249         if not len(str(e)):  # BdbQuit
1250             print("Exception: {0}".format(e.__class__.__name__))
1251             sys.exit(2)
1252
1253         print("Unhandled exception ({0}). Please report this as a bug at "
1254               "https://github.com/phillipberndt/autorandr/issues.".format(e),
1255               file=sys.stderr)
1256         raise
1257
1258
1259 if __name__ == '__main__':
1260     exception_handled_main()