idle.py 11.3 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Mateusz Biliński <mateusz AT bilinski.it>
# Copyright (C) 2008 Thorsten P. 'dGhvcnN0ZW5wIEFUIHltYWlsIGNvbQ==\n'.decode("base64")
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
21

Philipp Hörist's avatar
Philipp Hörist committed
22
import sys
23
import time
Yann Leboulanger's avatar
Yann Leboulanger committed
24 25
import ctypes
import ctypes.util
26 27 28
import logging
from gi.repository import Gio
from gi.repository import GLib
Philipp Hörist's avatar
Philipp Hörist committed
29
from gi.repository import GObject
30

André's avatar
André committed
31 32
from gajim.common import app
from gajim.common.const import Display
Philipp Hörist's avatar
Philipp Hörist committed
33
from gajim.common.const import IdleState
34

Philipp Hörist's avatar
Philipp Hörist committed
35
log = logging.getLogger('gajim.c.idle')
36 37


38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
class DBusFreedesktopIdleMonitor:

    def __init__(self):
        self.last_idle_time = 0
        self._extended_away = False

        log.debug('Connecting to D-Bus')
        self.dbus_proxy = Gio.DBusProxy.new_for_bus_sync(
            Gio.BusType.SESSION,
            Gio.DBusProxyFlags.NONE,
            None,
            'org.freedesktop.ScreenSaver',
            '/org/freedesktop/ScreenSaver',
            'org.freedesktop.ScreenSaver',
            None
        )
        log.debug('D-Bus connected')

        # Only the following call will trigger exceptions if the D-Bus
        # interface/method/... does not exist. Using the failing method
        # for class init to allow other idle monitors to be used on failure.
        self._get_idle_sec_fail()
        log.debug('D-Bus call test successful')

    def _get_idle_sec_fail(self):
        (idle_time,) = self.dbus_proxy.call_sync(
            'GetSessionIdleTime',
            None,
            Gio.DBusCallFlags.NO_AUTO_START,
            -1,
            None
        )
        return idle_time//1000

    def get_idle_sec(self):
        try:
            self.last_idle_time = self._get_idle_sec_fail()
        except GLib.Error as error:
            log.warning(
                'org.freedesktop.ScreenSaver.GetSessionIdleTime() failed: %s',
                error)

        return self.last_idle_time

    def set_extended_away(self, state):
        self._extended_away = state

    def is_extended_away(self):
        return self._extended_away


89 90 91 92
class DBusGnomeIdleMonitor:

    def __init__(self):
        self.last_idle_time = 0
Philipp Hörist's avatar
Philipp Hörist committed
93
        self._extended_away = False
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

        log.debug('Connecting to D-Bus')
        self.dbus_gnome_proxy = Gio.DBusProxy.new_for_bus_sync(
            Gio.BusType.SESSION,
            Gio.DBusProxyFlags.NONE,
            None,
            'org.gnome.Mutter.IdleMonitor',
            '/org/gnome/Mutter/IdleMonitor/Core',
            'org.gnome.Mutter.IdleMonitor',
            None
        )
        log.debug('D-Bus connected')

        # Only the following call will trigger exceptions if the D-Bus
        # interface/method/... does not exist. Using the failing method
        # for class init to allow other idle monitors to be used on failure.
        self._get_idle_sec_fail()
        log.debug('D-Bus call test successful')

    def _get_idle_sec_fail(self):
        (idle_time,) = self.dbus_gnome_proxy.call_sync(
Philipp Hörist's avatar
Philipp Hörist committed
115 116 117 118 119 120
            'GetIdletime',
            None,
            Gio.DBusCallFlags.NO_AUTO_START,
            -1,
            None
        )
121 122 123 124 125
        return int(idle_time / 1000)

    def get_idle_sec(self):
        try:
            self.last_idle_time = self._get_idle_sec_fail()
Philipp Hörist's avatar
Philipp Hörist committed
126
        except GLib.Error as error:
127 128
            log.warning(
                'org.gnome.Mutter.IdleMonitor.GetIdletime() failed: %s',
Philipp Hörist's avatar
Philipp Hörist committed
129
                error)
130 131 132

        return self.last_idle_time

Philipp Hörist's avatar
Philipp Hörist committed
133 134 135
    def set_extended_away(self, state):
        self._extended_away = state

Philipp Hörist's avatar
Philipp Hörist committed
136
    def is_extended_away(self):
Philipp Hörist's avatar
Philipp Hörist committed
137
        return self._extended_away
Yann Leboulanger's avatar
Yann Leboulanger committed
138 139


140 141
class XssIdleMonitor:
    def __init__(self):
Philipp Hörist's avatar
Philipp Hörist committed
142

Philipp Hörist's avatar
Philipp Hörist committed
143 144
        self._extended_away = False

Philipp Hörist's avatar
Philipp Hörist committed
145 146 147 148 149 150 151 152 153 154
        class XScreenSaverInfo(ctypes.Structure):
            _fields_ = [
                ('window', ctypes.c_ulong),
                ('state', ctypes.c_int),
                ('kind', ctypes.c_int),
                ('til_or_since', ctypes.c_ulong),
                ('idle', ctypes.c_ulong),
                ('eventMask', ctypes.c_ulong)
            ]

155 156 157 158 159 160 161
        XScreenSaverInfo_p = ctypes.POINTER(XScreenSaverInfo)

        display_p = ctypes.c_void_p
        xid = ctypes.c_ulong
        c_int_p = ctypes.POINTER(ctypes.c_int)

        libX11path = ctypes.util.find_library('X11')
Philipp Hörist's avatar
Philipp Hörist committed
162
        if libX11path is None:
163 164 165
            raise OSError('libX11 could not be found.')
        libX11 = ctypes.cdll.LoadLibrary(libX11path)
        libX11.XOpenDisplay.restype = display_p
166
        libX11.XOpenDisplay.argtypes = (ctypes.c_char_p,)
167
        libX11.XDefaultRootWindow.restype = xid
168
        libX11.XDefaultRootWindow.argtypes = (display_p,)
169 170

        libXsspath = ctypes.util.find_library('Xss')
Philipp Hörist's avatar
Philipp Hörist committed
171
        if libXsspath is None:
172 173 174 175
            raise OSError('libXss could not be found.')
        self.libXss = ctypes.cdll.LoadLibrary(libXsspath)
        self.libXss.XScreenSaverQueryExtension.argtypes = display_p, c_int_p, c_int_p
        self.libXss.XScreenSaverAllocInfo.restype = XScreenSaverInfo_p
Philipp Hörist's avatar
Philipp Hörist committed
176 177
        self.libXss.XScreenSaverQueryInfo.argtypes = (
            display_p, xid, XScreenSaverInfo_p)
178 179

        self.dpy_p = libX11.XOpenDisplay(None)
Philipp Hörist's avatar
Philipp Hörist committed
180
        if self.dpy_p is None:
181 182 183 184
            raise OSError('Could not open X Display.')

        _event_basep = ctypes.c_int()
        _error_basep = ctypes.c_int()
Philipp Hörist's avatar
Philipp Hörist committed
185 186 187
        extension = self.libXss.XScreenSaverQueryExtension(
            self.dpy_p, ctypes.byref(_event_basep), ctypes.byref(_error_basep))
        if extension == 0:
188 189 190
            raise OSError('XScreenSaver Extension not available on display.')

        self.xss_info_p = self.libXss.XScreenSaverAllocInfo()
Philipp Hörist's avatar
Philipp Hörist committed
191
        if self.xss_info_p is None:
192 193 194 195 196
            raise OSError('XScreenSaverAllocInfo: Out of Memory.')

        self.rootwindow = libX11.XDefaultRootWindow(self.dpy_p)

    def get_idle_sec(self):
Philipp Hörist's avatar
Philipp Hörist committed
197 198 199 200 201 202
        info = self.libXss.XScreenSaverQueryInfo(
            self.dpy_p, self.rootwindow, self.xss_info_p)
        if info == 0:
            return info
        return int(self.xss_info_p.contents.idle / 1000)

Philipp Hörist's avatar
Philipp Hörist committed
203 204 205
    def set_extended_away(self, state):
        self._extended_away = state

Philipp Hörist's avatar
Philipp Hörist committed
206 207 208 209 210 211
    def is_extended_away(self):
        return False


class WindowsIdleMonitor:
    def __init__(self):
Philipp Hörist's avatar
Philipp Hörist committed
212 213 214 215 216 217
        self.OpenInputDesktop = ctypes.windll.user32.OpenInputDesktop
        self.CloseDesktop = ctypes.windll.user32.CloseDesktop
        self.SystemParametersInfo = ctypes.windll.user32.SystemParametersInfoW
        self.GetTickCount = ctypes.windll.kernel32.GetTickCount
        self.GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo

218 219
        self._locked_time = None

Philipp Hörist's avatar
Philipp Hörist committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233
        class LASTINPUTINFO(ctypes.Structure):
            _fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)]

        self.lastInputInfo = LASTINPUTINFO()
        self.lastInputInfo.cbSize = ctypes.sizeof(self.lastInputInfo)

    def get_idle_sec(self):
        self.GetLastInputInfo(ctypes.byref(self.lastInputInfo))
        return float(self.GetTickCount() - self.lastInputInfo.dwTime) / 1000

    def is_extended_away(self):
        # Check if Screen Saver is running
        # 0x72 is SPI_GETSCREENSAVERRUNNING
        saver_runing = ctypes.c_int(0)
234 235
        info = self.SystemParametersInfo(
            0x72, 0, ctypes.byref(saver_runing), 0)
Philipp Hörist's avatar
Philipp Hörist committed
236 237
        if info and saver_runing.value:
            return True
238 239

        # Check if Screen is locked
240 241
        # Also a UAC prompt counts as locked
        # So just return True if we are more than 10 seconds locked
242
        desk = self.OpenInputDesktop(0, False, 0)
243
        unlocked = bool(desk)
244 245
        self.CloseDesktop(desk)

246 247 248 249 250 251 252 253 254 255 256
        if unlocked:
            self._locked_time = None
            return False

        if self._locked_time is None:
            self._locked_time = time.time()
            return False

        threshold = time.time() - 10
        if threshold > self._locked_time:
            return True
Philipp Hörist's avatar
Philipp Hörist committed
257 258


Philipp Hörist's avatar
Philipp Hörist committed
259 260 261 262 263 264 265 266 267
class IdleMonitor(GObject.GObject):

    __gsignals__ = {
        'state-changed': (
            GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
            None, # return value
            () # arguments
        )}

Philipp Hörist's avatar
Philipp Hörist committed
268
    def __init__(self):
Philipp Hörist's avatar
Philipp Hörist committed
269
        GObject.GObject.__init__(self)
Philipp Hörist's avatar
Philipp Hörist committed
270 271 272 273
        self.set_interval()
        self._state = IdleState.AWAKE
        self._idle_monitor = self._get_idle_monitor()

Philipp Hörist's avatar
Philipp Hörist committed
274 275 276
        if self.is_available():
            GLib.timeout_add_seconds(1, self._poll)

Philipp Hörist's avatar
Philipp Hörist committed
277 278 279 280 281 282
    def set_interval(self, away_interval=60, xa_interval=120):
        log.info('Set interval: away: %s, xa: %s',
                 away_interval, xa_interval)
        self._away_interval = away_interval
        self._xa_interval = xa_interval

Philipp Hörist's avatar
Philipp Hörist committed
283 284 285
    def set_extended_away(self, state):
        self._idle_monitor.set_extended_away(state)

Philipp Hörist's avatar
Philipp Hörist committed
286 287 288
    def is_available(self):
        return self._idle_monitor is not None

Philipp Hörist's avatar
Philipp Hörist committed
289
    @property
Philipp Hörist's avatar
Philipp Hörist committed
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    def state(self):
        if not self.is_available():
            return IdleState.UNKNOWN
        return self._state

    def is_xa(self):
        return self.state == IdleState.XA

    def is_away(self):
        return self.state == IdleState.AWAY

    def is_awake(self):
        return self.state == IdleState.AWAKE

    def is_unknown(self):
        return self.state == IdleState.UNKNOWN

Philipp Hörist's avatar
Philipp Hörist committed
307 308
    @staticmethod
    def _get_idle_monitor():
Philipp Hörist's avatar
Philipp Hörist committed
309 310 311 312
        if sys.platform == 'win32':
            return WindowsIdleMonitor()

        try:
313
            return DBusFreedesktopIdleMonitor()
Philipp Hörist's avatar
Philipp Hörist committed
314 315 316
        except GLib.Error as error:
            log.info('Idle time via D-Bus not available: %s', error)

317 318 319 320 321
        try:
            return DBusGnomeIdleMonitor()
        except GLib.Error as error:
            log.info('Idle time via D-Bus (GNOME) not available: %s', error)

André's avatar
André committed
322 323 324
        if app.is_display(Display.WAYLAND):
            return

325 326 327 328
        try:
            return XssIdleMonitor()
        except OSError as error:
            log.info('Idle time via XScreenSaverInfo not available: %s', error)
Philipp Hörist's avatar
Philipp Hörist committed
329 330 331 332

    def get_idle_sec(self):
        return self._idle_monitor.get_idle_sec()

Philipp Hörist's avatar
Philipp Hörist committed
333
    def _poll(self):
Philipp Hörist's avatar
Philipp Hörist committed
334 335 336 337 338
        """
        Check to see if we should change state
        """
        if self._idle_monitor.is_extended_away():
            log.info('Extended Away: Screensaver or Locked Screen')
Philipp Hörist's avatar
Philipp Hörist committed
339
            self._set_state(IdleState.XA)
Philipp Hörist's avatar
Philipp Hörist committed
340 341 342 343 344 345
            return True

        idle_time = self.get_idle_sec()

        # xa is stronger than away so check for xa first
        if idle_time > self._xa_interval:
Philipp Hörist's avatar
Philipp Hörist committed
346
            self._set_state(IdleState.XA)
Philipp Hörist's avatar
Philipp Hörist committed
347
        elif idle_time > self._away_interval:
Philipp Hörist's avatar
Philipp Hörist committed
348
            self._set_state(IdleState.AWAY)
349
        else:
Philipp Hörist's avatar
Philipp Hörist committed
350
            self._set_state(IdleState.AWAKE)
Philipp Hörist's avatar
Philipp Hörist committed
351 352
        return True

Philipp Hörist's avatar
Philipp Hörist committed
353 354 355 356 357 358 359 360
    def _set_state(self, state):
        if self._state == state:
            return

        self._state = state
        log.info('State changed: %s', state)
        self.emit('state-changed')

Philipp Hörist's avatar
Philipp Hörist committed
361 362

Monitor = IdleMonitor()