Files
upstream-aw-watcher-afk/aw_watcher_afk/afk.py
upstream-mirror 87aaa2add8
Some checks failed
Build / Test on macos-latest (py-3.9) (push) Has been cancelled
Build / Test on ubuntu-24.04 (py-3.9) (push) Has been cancelled
Build / Test on windows-latest (py-3.9) (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1m36s
snapshot of https://github.com/ActivityWatch/aw-watcher-afk.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:21:02 +05:00

137 lines
5.0 KiB
Python
Executable File

import logging
import os
import platform
from datetime import datetime, timedelta, timezone
from time import sleep
from aw_client import ActivityWatchClient
from aw_core.models import Event
from .config import load_config
system = platform.system()
if system == "Windows":
# noreorder
from .windows import seconds_since_last_input # fmt: skip
elif system == "Darwin":
# noreorder
from .macos import seconds_since_last_input # fmt: skip
elif system == "Linux":
# noreorder
from .unix import seconds_since_last_input # fmt: skip
else:
raise Exception(f"Unsupported platform: {system}")
logger = logging.getLogger(__name__)
td1ms = timedelta(milliseconds=1)
class Settings:
def __init__(self, config_section, timeout=None, poll_time=None):
# Time without input before we're considering the user as AFK
self.timeout = timeout or config_section["timeout"]
# How often we should poll for input activity
self.poll_time = poll_time or config_section["poll_time"]
assert self.timeout >= self.poll_time
class AFKWatcher:
def __init__(self, args, testing=False):
# Read settings from config
self.settings = Settings(
load_config(testing), timeout=args.timeout, poll_time=args.poll_time
)
self.client = ActivityWatchClient(
"aw-watcher-afk", host=args.host, port=args.port, testing=testing
)
self.bucketname = "{}_{}".format(
self.client.client_name, self.client.client_hostname
)
# Store initial parent PID for orphan detection
self._initial_ppid = os.getppid()
def ping(self, afk: bool, timestamp: datetime, duration: float = 0):
data = {"status": "afk" if afk else "not-afk"}
e = Event(timestamp=timestamp, duration=duration, data=data)
pulsetime = self.settings.timeout + self.settings.poll_time
self.client.heartbeat(self.bucketname, e, pulsetime=pulsetime, queued=True)
def run(self):
logger.info("aw-watcher-afk started")
# Initialization
self.client.wait_for_start()
eventtype = "afkstatus"
self.client.create_bucket(self.bucketname, eventtype, queued=True)
# Start afk checking loop
with self.client:
self.heartbeat_loop()
def heartbeat_loop(self):
afk = False
while True:
try:
if system in ["Darwin", "Linux"] and os.getppid() != self._initial_ppid:
logger.info(
"afkwatcher stopped because parent process died "
f"(ppid changed from {self._initial_ppid} to {os.getppid()})"
)
break
now = datetime.now(timezone.utc)
try:
seconds_since_input = seconds_since_last_input()
except OSError:
if system != "Windows":
raise
logger.exception(
"seconds_since_last_input() failed; "
"retrying without changing AFK state"
)
sleep(self.settings.poll_time)
continue
last_input = now - timedelta(seconds=seconds_since_input)
logger.debug(f"Seconds since last input: {seconds_since_input}")
# If no longer AFK
if afk and seconds_since_input < self.settings.timeout:
logger.info("No longer AFK")
self.ping(afk, timestamp=last_input)
afk = False
# ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
self.ping(afk, timestamp=last_input + td1ms)
# If becomes AFK
elif not afk and seconds_since_input >= self.settings.timeout:
logger.info("Became AFK")
self.ping(afk, timestamp=last_input)
afk = True
# ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
self.ping(
afk, timestamp=last_input + td1ms, duration=seconds_since_input
)
# Send a heartbeat if no state change was made
else:
if afk:
# we need the +1ms here too, to make sure we don't "miss" the last heartbeat
# (if last_input hasn't changed)
self.ping(
afk,
timestamp=last_input + td1ms,
duration=seconds_since_input,
)
else:
self.ping(afk, timestamp=last_input)
sleep(self.settings.poll_time)
except KeyboardInterrupt:
logger.info("aw-watcher-afk stopped by keyboard interrupt")
break