diff --git a/i3pystatus/abc_radio.py b/i3pystatus/abc_radio.py index 8ee5589..401e602 100644 --- a/i3pystatus/abc_radio.py +++ b/i3pystatus/abc_radio.py @@ -57,6 +57,8 @@ class ABCRadio(IntervalModule): # Destroy the player after this many seconds of inactivity PLAYER_LIFETIME = 5 + # Do not suspend the player when i3bar is hidden. + keep_alive = True show_info = {} player = None station_info = None diff --git a/i3pystatus/core/io.py b/i3pystatus/core/io.py index c51fe1d..ca45bd7 100644 --- a/i3pystatus/core/io.py +++ b/i3pystatus/core/io.py @@ -1,9 +1,11 @@ import json import signal import sys + from contextlib import contextmanager from threading import Condition from threading import Thread +from i3pystatus.core.modules import IntervalModule class IOHandler: @@ -54,7 +56,12 @@ class StandaloneIO(IOHandler): n = -1 proto = [ - {"version": 1, "click_events": True}, "[", "[]", ",[]", + { + "version": 1, + "click_events": True, + "stop_signal": signal.SIGUSR2, + "cont_signal": signal.SIGUSR2 + }, "[", "[]", ",[]", ] def __init__(self, click_events, modules, interval=1): @@ -72,7 +79,10 @@ class StandaloneIO(IOHandler): self.refresh_cond = Condition() self.treshold_interval = 20.0 + + self.stopped = False signal.signal(signal.SIGUSR1, self.refresh_signal_handler) + signal.signal(signal.SIGUSR2, self.suspend_signal_handler) def read(self): self.compute_treshold_interval() @@ -142,6 +152,26 @@ class StandaloneIO(IOHandler): self.async_refresh() + def suspend_signal_handler(self, signo, frame): + """ + By default, i3bar sends SIGSTOP to all children when it is not visible (for example, the screen + sleeps or you enter full screen mode). This stops the i3pystatus process and all threads within it. + For some modules, this is not desirable. Thankfully, the i3bar protocol supports setting the "stop_signal" + and "cont_signal" key/value pairs in the header to allow sending a custom signal when these events occur. + + Here we use SIGUSR2 for both "stop_signal" and "cont_signal" and maintain a toggle to determine whether + we have just been stopped or continued. When we have been stopped, notify the IntervalModule managers + that they should suspend any module that does not set the keep_alive flag to a truthy value, and when we + have been continued, notify the IntervalModule managers that they can resume execution of all modules. + """ + if signo != signal.SIGUSR2: + return + self.stopped = not self.stopped + if self.stopped: + [m.suspend() for m in IntervalModule.managers.values()] + else: + [m.resume() for m in IntervalModule.managers.values()] + class JSONIO: def __init__(self, io, skiplines=2): diff --git a/i3pystatus/core/threading.py b/i3pystatus/core/threading.py index 1e3d8bb..6e4a475 100644 --- a/i3pystatus/core/threading.py +++ b/i3pystatus/core/threading.py @@ -1,18 +1,25 @@ import threading import time import sys -import traceback from i3pystatus.core.util import partition timer = time.perf_counter if hasattr(time, "perf_counter") else time.clock +def unwrap_workload(workload): + """ Obtain the module from it's wrapper. """ + while isinstance(workload, Wrapper): + workload = workload.workload + return workload + + class Thread(threading.Thread): def __init__(self, target_interval, workloads=None, start_barrier=1): super().__init__() self.workloads = workloads or [] self.target_interval = target_interval self.start_barrier = start_barrier + self._suspended = threading.Event() self.daemon = True def __iter__(self): @@ -37,9 +44,20 @@ class Thread(threading.Thread): def execute_workloads(self): for workload in self: - workload() + if self.should_execute(workload): + workload() self.workloads.sort(key=lambda workload: workload.time) + def should_execute(self, workload): + """ + If we have been suspended by i3bar, only execute those modules that set the keep_alive flag to a truthy + value. See the docs on the suspend_signal_handler method of the io module for more information. + """ + if not self._suspended.is_set(): + return True + workload = unwrap_workload(workload) + return hasattr(workload, 'keep_alive') and getattr(workload, 'keep_alive') + def run(self): while self: self.execute_workloads() @@ -53,6 +71,12 @@ class Thread(threading.Thread): return [remove] + self.branch(vtime - remove.time, bound) return [] + def suspend(self): + self._suspended.set() + + def resume(self): + self._suspended.clear() + class Wrapper: def __init__(self, workload): @@ -143,3 +167,11 @@ class Manager: def start(self): for thread in self.threads: thread.start() + + def suspend(self): + for thread in self.threads: + thread.suspend() + + def resume(self): + for thread in self.threads: + thread.resume() diff --git a/i3pystatus/network.py b/i3pystatus/network.py index d449e1a..973f899 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -316,6 +316,8 @@ class Network(IntervalModule, ColorRangeModule): ("detect_active", "Attempt to detect the active interface"), ) + # Continue processing statistics when i3bar is hidden. + keep_alive = True interval = 1 interface = 'eth0'