Add fact collection with xrandr

This commit is contained in:
Alexander Grothe
2017-04-10 13:24:40 +02:00
parent bcbdc8308b
commit a788fdcb4d
8 changed files with 888 additions and 344 deletions

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env python2
DOCUMENTATION = '''
---
module: xorg_facts
short_description: "gather facts about connected monitors and available modelines"
description:
- This script needs a running x-server on a given display in order to successfully call xrandr.
The ranking uses the following factors:
1. preferred_refreshrate
2. preferred_resolution
3. preferred_output
For each element a dictionary of values (up to 4 bit [0 .. 256]) may be passed to the module.
The rank is represented by this order of 4-Bit values:
| rrate | resolution | output | internal score
| 50 | 1920x1080 | HDMI | 0b_0100_0100_0100 = 1092
| 60 | 1280x720 | DP | 0b_0011_0011_0011 = 819
Returns the connected output, monitors and modelines and a suggestion for the most fitting mode in a dictionary 'xorg'
options:
display:
required: False
default: ":0"
description:
- the DISPLAY variable to use when calling xrandr
preferred_outpus:
required: False
default: {"HDMI": 4, "DP": 3, "DVI": 2, "VGA": 1, "TV": 0}
description:
- ranking of the preferred display connectors
preferred_refreshrates:
required: False
default: {"50": 4, "60": 3, "75": 2, "30": 1, "25": 0}
description:
- ranking of the preferred display refreshrate
preferred_resolutions:
required: False
default: {"7680x4320": 8, "3840x2160": 4, "1920x1080": 2, "1280x720": 1, "720x576": 0}
description:
- ranking of the preferred display resolutions
'''
EXAMPLES = '''
- name: "collect facts for connected displays"
action: xserver_facts
display: ":0"
- debug:
var: xorg
'''
import ast
import json
import re
import subprocess
import sys
import time
from collections import OrderedDict, namedtuple
from ansible.module_utils.basic import *
arg_specs = {
'display': dict(default=[":0", ":0.1"], type='list', required=False),
'multi_display': dict(default=True, type='bool', required=False),
'preferred_outputs': dict(default={"HDMI": 8, "DP": 4, "DVI": 2, "VGA": 1, "TV": 0}, type='dict', required=False),
'preferred_refreshrates': dict(default={50: 8, 60: 4, 75: 3, 30: 2, 25: 1}, type='dict', required=False),
'preferred_resolutions': dict(default={"7680x4320": 8, "3840x2160": 4, "1920x1080": 2, "1280x720": 1, "720x576": 0},
type='dict', required=False),
}
Mode = namedtuple('Mode', ['connection', 'resolution', 'refreshrate'])
class ModelineTools(object):
def __init__(self, preferred_outputs, preferred_resolutions, preferred_refreshrates):
self.preferred_outputs = preferred_outputs
self.preferred_resolutions = preferred_resolutions
self.preferred_refreshrates = preferred_refreshrates
def get_score(self, connection, resolution, refreshrate):
connection = connection.split('-')[0]
score = self.preferred_refreshrates.get(int(refreshrate), 0)
score = score << 4
score += self.preferred_resolutions.get(resolution, 0)
#score = score << 4
#score += self.preferred_outputs.get(connection, 0)
return score
@staticmethod
def cleanup_refreshrate(refreshrate):
rrate = refreshrate.replace('+', '').replace('*', '').replace(' ', '').strip()
return int(round(ast.literal_eval(rrate)))
def sort_mode(self, mode):
refreshrate_score = self.preferred_refreshrates.get(int(mode.refreshrate), 0)
resolution_score = self.preferred_resolutions.get(mode.resolution, 0)
x, y = mode.resolution.split('x')
connection = mode.connection.split('-')[0]
return (refreshrate_score, resolution_score, int(x), int(y), self.preferred_outputs.get(connection, 0))
def main():
module = AnsibleModule(argument_spec=arg_specs, supports_check_mode=False,)
display_list = module.params['display']
preferred_outputs = module.params['preferred_outputs']
preferred_resolutions = module.params['preferred_resolutions']
preferred_refreshrates = module.params['preferred_refreshrates']
mtools = ModelineTools(preferred_outputs, preferred_resolutions, preferred_refreshrates)
modes = []
displays = {}
data = {}
for display in display_list:
# call xrandr
try:
xrandr_data = subprocess.check_output(['xrandr', '-q','-d', display],
universal_newlines=True)
except: continue
for line in xrandr_data.splitlines():
if line.startswith('Screen'):
screen = line.split(':')[0].split()[-1]
screen = "Screen{}".format(screen)
displays[screen] = {}
elif 'connected' in line:
connection = line.split()[0]
displays[screen][connection] = {}
if 'disconnected' in line:
displays[screen][connection]['connected'] = False
else:
displays[screen][connection]['connected'] = True
displays[screen][connection]['modes'] = OrderedDict(
sorted({}.items(), key=lambda t: t.split('_')[0]))
display_modes = []
elif line.startswith(' '):
fields = filter(None, re.split(r'\s{2,}', line))
resolution = fields[0]
refreshrates = fields[1:]
r = set()
for refreshrate in refreshrates:
refreshrate = refreshrate.strip()
rrate = mtools.cleanup_refreshrate(refreshrate)
if len(refreshrate) < 2:
continue
if '*' in refreshrate:
current_mode = Mode(connection, resolution, rrate)
displays[screen][connection]['current_mode'] = current_mode
if '+' in refreshrate:
preferred_mode = Mode(connection, resolution, rrate)
displays[screen][connection]['preferred_mode'] = preferred_mode
r.add(mtools.cleanup_refreshrate(refreshrate))
modes.append(Mode(connection=connection, resolution=resolution, refreshrate=rrate))
display_modes.append(Mode(connection=connection, resolution=resolution, refreshrate=rrate))
displays[screen][connection]['modes'][resolution] = sorted(r)
displays[screen][connection]['best_mode'] = max(display_modes, key=mtools.sort_mode)
data['displays'] = displays
#data['modes'] = modes
best_mode = max(modes, key=mtools.sort_mode)
data["best_mode"] = {
'connection': best_mode.connection,
'resolution': best_mode.resolution,
'refreshrate': best_mode.refreshrate,
}
module.exit_json(changed=False, ansible_facts={'xorg': data})
if __name__ == '__main__':
main()

196
library/xrandr_facts.py Normal file
View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python2
from __future__ import print_function
import ast
import binascii
import re
import subprocess
from collections import namedtuple
from ansible.module_utils.basic import *
DOCUMENTATION = '''
---
module: xrandr_facts
short_description: "gather facts about connected monitors and available modelines"
description:
- This module needs a running x-server on a given display in order to successfully call xrandr.
Returns the dictionary "xrandr", wich contains all screens with output states, connected displays,
EDID info and their modes and a recommendation for the best fitting tv mode.
options:
display:
required: False
default: ":0"
description:
- the DISPLAY variable to use when calling xrandr
multi_display:
required: False
default: "False"
description:
- check additional screens (:0.0 .. :0.n) until xrandr fails to collect information
preferred_outpus:
required: False
default: ["HDMI", "DP", "DVI", "VGA", "TV": 0]
description:
- ranking of the preferred display connectors
preferred_refreshrates:
required: False
default: ["50", "60", "75", "30", "25"]
description:
- ranking of the preferred display refreshrate
preferred_resolutions:
required: False
default: ["7680x4320", "3840x2160", "1920x1080", "1280x720", "720x576"]
description:
- ranking of the preferred display resolutions
'''
EXAMPLES = '''
- name: "collect facts for connected displays"
action: xserver_facts
display: ":0"
- debug:
var: xrandr
'''
ARG_SPECS = {
'display': dict(default=":0", type='str', required=False),
'multi_display': dict(default=False, type='bool', required=False),
'preferred_outputs': dict(
default=["HDMI", "DP", "DVI", "VGA", "TV"], type='list', required=False),
'preferred_refreshrates': dict(
default=[50, 60, 75, 30, 25], type='list', required=False),
'preferred_resolutions': dict(
default=[
"7680x4320", "3840x2160", "1920x1080", "1280x720", "720x576"],
type='list', required=False),
}
SCREEN_REGEX = re.compile("^(?P<screen>Screen\s\d+:)(?:.*)")
CONNECTOR_REGEX = re.compile(
"^(?P<connector>.*-\d+)\s(?P<connection_state>connected|disconnected)\s(?P<primary>primary)?")
MODE_REGEX = re.compile("^\s+(?P<resolution>\d{3,}x\d{3,}).*")
Mode = namedtuple('Mode', ['connection', 'resolution', 'refreshrate'])
def check_for_screen(line):
"""check line for screen information"""
match = re.match(SCREEN_REGEX, line)
if match:
return match.groupdict()['screen']
def check_for_connection(line):
"""check line for connection name and state"""
match = re.match(CONNECTOR_REGEX, line)
connector = None
is_connected = False
if match:
match = match.groupdict()
connector = match['connector']
is_connected = True if match['connection_state'] == 'connected' else False
return connector, is_connected
def get_indentation(line):
"""return the number of leading whitespace characters"""
return len(line) - len(line.lstrip())
def sort_mode(mode):
"""rate modes by several criteria"""
connection_score = 0
rrate_score = 0
resolution_score = 0
preferred_rrates = module.params['preferred_refreshrates']
# [50, 60]
preferred_resolutions = module.params['preferred_resolutions']
# ["7680x4320", "3840x2160", "1920x1080", "1280x720", "720x576"]
preferred_outputs = module.params['preferred_outputs']
# ["HDMI", "DP", "DVI", "VGA"]
if mode.refreshrate in preferred_rrates:
rrate_score = len(preferred_rrates) - preferred_rrates.index(mode.refreshrate)
if mode.resolution in preferred_resolutions:
resolution_score = len(preferred_resolutions) - preferred_resolutions.index(mode.resolution)
x_resolution, y_resolution = (int(n) for n in mode.resolution.split('x'))
connection = mode.connection.split('-')[0]
if connection in preferred_outputs:
connection_score = len(preferred_outputs) - preferred_outputs.index(connection)
return (rrate_score, resolution_score, x_resolution, y_resolution, connection_score)
def parse_xrandr_verbose(iterator):
"""parse the output of xrandr --verbose using an iterator delivering single lines"""
xorg = {}
is_connected = False
for line in iterator:
if line.startswith('Screen'):
screen = check_for_screen(line)
xorg[screen] = {}
elif 'connected' in line:
connector, is_connected = check_for_connection(line)
xorg[screen][connector] = {
'is_connected': is_connected,
'EDID': '',
'modes': {},
'preferred': '',
'current': '',
'auto': '',
}
elif is_connected and 'EDID:' in line:
edid_str = ""
outer_indentation = get_indentation(line)
while True:
line = next(iterator)
if get_indentation(line) > outer_indentation:
edid_str += line.strip()
else:
break
xorg[screen][connector]['EDID'] = edid_str
elif is_connected and "MHz" in line and not "Interlace" in line:
match = re.match(MODE_REGEX, line)
if match:
match = match.groupdict()
preferred = bool("+preferred" in line)
current = bool("*current" in line)
while True:
line = next(iterator)
if line.strip().startswith('v:'):
refresh_rate = ast.literal_eval(line.split()[-1][:-2])
rrate = int(round(refresh_rate))
if xorg[screen][connector]['modes'].get(match['resolution']) is None:
xorg[screen][connector]['modes'][match['resolution']] = []
if rrate not in xorg[screen][connector]['modes'][match['resolution']]:
xorg[screen][connector]['modes'][match['resolution']].append(rrate)
if preferred:
xorg[screen][connector]['preferred'] = "{}_{}".format(
match['resolution'], rrate)
if current:
xorg[screen][connector]['current'] = "{}_{}".format(
match['resolution'], rrate)
break
return xorg
def output_data(data):
if data:
modes = []
for _, screen_data in data.items():
for connector, connection_data in screen_data.items():
if connection_data.get('EDID'):
with open('/etc/X11/edid.{}.bin'.format(connector), 'wb') as edid:
edid.write(binascii.a2b_hex(connection_data['EDID']))
for resolution, refreshrates in connection_data['modes'].items():
for refreshrate in refreshrates:
modes.append(Mode(connector, resolution, refreshrate))
if modes:
best_mode = max(modes, key=sort_mode)
data['best_tv_mode'] = best_mode
#print(json.dumps(data, sort_keys=True, indent=4))
module.exit_json(changed=False, ansible_facts={'xrandr': data})
if __name__ == '__main__':
module = AnsibleModule(argument_spec=ARG_SPECS, supports_check_mode=False,)
try:
d = subprocess.check_output(['xrandr', '-d', module.params['display'], '--verbose'], universal_newlines=True).splitlines()
except subprocess.CalledProcessError:
xorg_data = {}
else:
xorg_data = parse_xrandr_verbose(iter(d))
output_data(xorg_data)