Skip to content

Commit

Permalink
Add module "keep_alive" feature. (enkore#601)
Browse files Browse the repository at this point in the history
By default, i3bar sends SIGSTOP to all children when it is not visible (for example, the screen
sleeps or you enter full screen mode), stopping the i3pystatus process and all threads within it.
For some modules, this is not desirable. This commit makes it possible for modules to define the
"keep_alive" flag to indicate that they would like to continue executing regardless of the state of
i3bar.
  • Loading branch information
facetoe committed Aug 10, 2017
1 parent 13a1cac commit c044d1f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 3 deletions.
2 changes: 2 additions & 0 deletions i3pystatus/abc_radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion i3pystatus/core/io.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -72,7 +79,10 @@ def __init__(self, click_events, modules, interval=1):

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()
Expand Down Expand Up @@ -142,6 +152,26 @@ def refresh_signal_handler(self, signo, frame):

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):
Expand Down
36 changes: 34 additions & 2 deletions i3pystatus/core/threading.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -37,9 +44,20 @@ def wait_for_start_barrier(self):

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()
Expand All @@ -53,6 +71,12 @@ def branch(self, vtime, bound):
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):
Expand Down Expand Up @@ -143,3 +167,11 @@ def append(self, workload):
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()
2 changes: 2 additions & 0 deletions i3pystatus/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down

0 comments on commit c044d1f

Please sign in to comment.