commit f9fb39bfcc2b933e6bd4627565919963905a8ec6 Author: Thiago Macedo Date: Mon Mar 11 23:48:40 2019 -0300 working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5acb295 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +usage/* +!usage/.gitkeep diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..8436f68 --- /dev/null +++ b/config.yaml @@ -0,0 +1,11 @@ +check-interval: 1 +timers: + gaming: + apps: + - retroarch + - minecraft + - atom + # how many minutes of usage + time-limit: 3 + # within how many hours of interval + limit-interval: 0.1 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3d947f7 --- /dev/null +++ b/readme.md @@ -0,0 +1,33 @@ +# App Timer (Parental Control) + +Control apps usage time with a command line application. + +The python daemon will watch for the apps runtime (with `ps`) and store timing information about it's usage. It can block (`kill`) an app if the usage is off limits. + +I'm using this at a raspberry pi gaming station to enforce usage limits. + +## Config + + # how often should the script check for apps runtime? + check-interval: 1 + # timers: list of apps that will be watched + timers: + # the app name (list key) + gaming: + # list of the app executables + apps: + - retroarch + - minecraft + # how many minutes of usage + time-limit: 60 + # within how many hours of interval + limit-interval: 12 + +## Installation + +There's a systemd service file ready for use. + + cp ./timer.service /etc/systemd/system/app-timer.service + systemctl enable app-timer + +Update this file to point `ExecStart` into your setup folder. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8786e3f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +yaml diff --git a/timer.py b/timer.py new file mode 100644 index 0000000..c45355f --- /dev/null +++ b/timer.py @@ -0,0 +1,182 @@ +import os, time +import subprocess + +from yaml import load, dump, Loader, Dumper + +CONFIG_PATH = os.path.dirname(os.path.abspath(__file__)) + +def shell(cmd): + # cmd = cmd.split(' ') + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True) + # p.communicate() + return p.stdout.read() + + +class Usage(object): + """Usage Storage and Control""" + timer = None + file = None + + def __init__(self, timer): + super(Usage, self).__init__() + self.timer = timer + self.file = '%s/usage/%s' % (CONFIG_PATH, self.timer.name) + + @property + def current(self): + '''Current usage of this timer in minutes''' + if not os.path.isfile(self.file): + return 0 + with open(self.file, 'r') as f: + current = f.read() + return int(current) if current else 0 + + def increment(self, time): + '''Increment this timer usage''' + usage = self.current + int(time) + with open(self.file, 'w') as f: + f.write(str(usage)) + + def release(self): + '''Remove the timer usage to restart the counter''' + os.remove(self.file) + + def isOffLimit(self): + '''Is the timer usage off its limits''' + time_limit = self.timer.timeLimit + if (time_limit < 0): + return False + if self.current > time_limit: + return True + + def isOffInterval(self): + '''Is the timer usage expired''' + limit_interval = self.timer.limitInterval + if (limit_interval < 0): + return False + if not os.path.exists(self.file): + return False + usage_started = os.path.getctime(self.file) + usage_expiry = usage_started + (limit_interval * 60 * 60) + print(usage_started, usage_expiry) + return time.time() >= usage_expiry + + +class Timer(object): + """Timer Setup and Apps Management""" + item = None + name = None + usage = None + + def __init__(self, name, item): + super(Timer, self).__init__() + self.name = name + self.item = item + self.usage = Usage(self) + + @property + def timeLimit(self): + time_limit = self.item.get('time-limit', -1) + return int(time_limit) + @property + def limitInterval(self): + limit_interval = self.item.get('limit-interval', -1) + return float(limit_interval) + + @property + def apps(self): + item_apps = self.item.get('apps', []) + if not isinstance(item_apps, list): + item_apps = [item_apps] + apps = [app.strip() for app in item_apps] + return apps + + def isRunning(self): + running = False + for app in self.apps: + cmd = 'ps aux | grep -v grep | grep "%s"' % app + res = shell(cmd) + if len(res): + running = True + return running + + def block(self): + for app in self.apps: + cmd = 'killall "%s"' % app + shell(cmd) + + +class Config(object): + """Config Parser""" + data = None + timers = None + mtime = 0 + + def __init__(self): + super(Config, self).__init__() + self.file = '%s/config.yaml' % CONFIG_PATH + self.reload() + + def hasChanges(self): + '''Check if the config file has changes''' + mtime = os.path.getmtime(self.file) + return self.mtime != mtime + + def reload(self): + '''Reload config file data''' + self.mtime = os.path.getmtime(self.file) + self.data = self.read() + self.timers = self.getTimers() + + def read(self): + '''Read the config file yaml object''' + with open(self.file, 'r') as f: + data = load(f, Loader=Loader) + return data + + def getTimers(self): + '''Gets a generator of Timer objects''' + config_timers = self.data.get('timers', {}) + timers = [] + for name, item in config_timers.items(): + timers.append(Timer(name, item)) + return timers + + @property + def checkInterval(self): + '''How often the usage check should happen''' + interval = self.data.get('check-interval', 1) + return int(interval) + + +def check_timers(config): + '''Will check every timer setup for it's usage and limits''' + for timer in config.timers: + if not timer.isRunning(): + continue + print('Timer %s is running' % timer.name) + # restore timers after interval + if timer.usage.isOffInterval(): + print('Timer %s is off interval' % timer.name) + timer.usage.release() + # check for off limit apps + if timer.usage.isOffLimit(): + print('Timer %s is off limit' % timer.name) + timer.block() + # increment running apps timer + timer.usage.increment(config.checkInterval) + + +config = Config() +while True: + # check config changes + if config.hasChanges(): + print('Config has changes') + config.reload() + # check app timers + check_timers(config) + # wait interval in minutes + time.sleep(-time.time() % (config.checkInterval * 60)) diff --git a/usage/.gitkeep b/usage/.gitkeep new file mode 100644 index 0000000..e69de29