# controller.py -- Redshift child process controller # This file is part of redshift-ng. # redshift-ng is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # redshift-ng is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with redshift-ng. If not, see . # Copyright (c) 2013-2017 Jon Lund Steffensen import os import re import fcntl import signal import gi gi.require_version('GLib', '2.0') from gi.repository import GLib, GObject from . import defs class RedshiftController(GObject.GObject): """GObject wrapper around the Redshift child process.""" __gsignals__ = { 'inhibit-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 'temperature-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 'period-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), 'location-changed': (GObject.SIGNAL_RUN_FIRST, None, (float, float)), 'error-occured': (GObject.SIGNAL_RUN_FIRST, None, (str,)), 'stopped': (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, args): """Initialize controller and start child process. The parameter args is a list of command line arguments to pass on to the child process. The "-v" argument is automatically added. """ GObject.GObject.__init__(self) # Initialize state variables self._inhibited = False self._temperature = 0 self._period = 'Unknown' self._location = (0.0, 0.0) # Start redshift with arguments args.insert(0, os.path.join(defs.BINDIR, 'redshift')) if '-v' not in args: args.insert(1, '-v') # Start child process with C locale so we can parse the output env = os.environ.copy() for key in ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES'): env[key] = 'C' self._process = GLib.spawn_async( args, envp=['{}={}'.format(k, v) for k, v in env.items()], flags=GLib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True, standard_error=True) # Wrap remaining contructor in try..except to avoid that the child # process is not closed properly. try: # Handle child input # The buffer is encapsulated in a class so we # can pass an instance to the child callback. class InputBuffer(object): buf = '' self._input_buffer = InputBuffer() self._error_buffer = InputBuffer() self._errors = '' # Set non blocking fcntl.fcntl( self._process[2], fcntl.F_SETFL, fcntl.fcntl(self._process[2], fcntl.F_GETFL) | os.O_NONBLOCK) # Add watch on child process GLib.child_watch_add( GLib.PRIORITY_DEFAULT, self._process[0], self._child_cb) GLib.io_add_watch( self._process[2], GLib.PRIORITY_DEFAULT, GLib.IO_IN, self._child_data_cb, (True, self._input_buffer)) GLib.io_add_watch( self._process[3], GLib.PRIORITY_DEFAULT, GLib.IO_IN, self._child_data_cb, (False, self._error_buffer)) # Signal handler to relay USR1 signal to redshift process def relay_signal_handler(signal): os.kill(self._process[0], signal) return True GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGUSR1, relay_signal_handler, signal.SIGUSR1) except: self.termwait() raise @property def inhibited(self): """Current inhibition state.""" return self._inhibited @property def temperature(self): """Current screen temperature.""" return self._temperature @property def period(self): """Current period of day.""" return self._period @property def location(self): """Current location.""" return self._location def set_inhibit(self, inhibit): """Set inhibition state.""" if inhibit != self._inhibited: self._child_toggle_inhibit() def _child_signal(self, sg): """Send signal to child process.""" os.kill(self._process[0], sg) def _child_toggle_inhibit(self): """Sends a request to the child process to toggle state.""" self._child_signal(signal.SIGUSR1) def _child_cb(self, pid, status, data=None): """Called when the child process exists.""" # Empty stdout and stderr for f in (self._process[2], self._process[3]): while True: buf = os.read(f, 256).decode('utf-8') if buf == '': break if f == self._process[3]: # stderr self._errors += buf # Check exit status of child try: GLib.spawn_check_exit_status(status) except GLib.GError: self.emit('error-occured', self._errors) GLib.spawn_close_pid(self._process[0]) self.emit('stopped') def _child_key_change_cb(self, key, value): """Called when the child process reports a change of internal state.""" def parse_coord(s): """Parse coordinate like `42.0 N` or `91.5 W`.""" v, d = s.split(' ') return float(v) * (1 if d in 'NE' else -1) if key == 'Status': new_inhibited = value != 'Enabled' if new_inhibited != self._inhibited: self._inhibited = new_inhibited self.emit('inhibit-changed', new_inhibited) elif key == 'Color temperature': new_temperature = int(value.rstrip('K'), 10) if new_temperature != self._temperature: self._temperature = new_temperature self.emit('temperature-changed', new_temperature) elif key == 'Period': new_period = value if new_period != self._period: self._period = new_period self.emit('period-changed', new_period) elif key == 'Location': new_location = tuple(parse_coord(x) for x in value.split(', ')) if new_location != self._location: self._location = new_location self.emit('location-changed', *new_location) def _child_stdout_line_cb(self, line): """Called when the child process outputs a line to stdout.""" if line: m = re.match(r'([\w ]+): (.+)', line) if m: key = m.group(1) value = m.group(2) self._child_key_change_cb(key, value) def _child_data_cb(self, f, cond, data): """Called when the child process has new data on stdout/stderr.""" stdout, ib = data ib.buf += os.read(f, 256).decode('utf-8') # Split input at line break while True: first, sep, last = ib.buf.partition('\n') if sep == '': break ib.buf = last if stdout: self._child_stdout_line_cb(first) else: self._errors += first + '\n' return True def terminate_child(self): """Send SIGINT to child process.""" self._child_signal(signal.SIGINT) def kill_child(self): """Send SIGKILL to child process.""" self._child_signal(signal.SIGKILL)