Skip to content

Commit 77a1fda

Browse files
committed
Change from threads to asyncio
Each plugin is run as an asyncio task rather than a slightly more resource heavy system thread. Also make a couple of minor improvements and add a test directory.
1 parent c90e71b commit 77a1fda

File tree

9 files changed

+86
-73
lines changed

9 files changed

+86
-73
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ upload: sdist
1919
twine3 upload --skip-existing dist/*
2020

2121
check:
22-
flake8 $(PYNAME).py setup.py
22+
ruff .
2323
vermin --no-tips -i $(PYNAME).py setup.py
2424
python3 setup.py check
2525
shellcheck plugins/*

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ to execute the sleep inhibition lock.
7474
AUR](https://aur.archlinux.org/packages/sleep-inhibitor) then skip to
7575
the next Configuration section.
7676

77-
Python 3.6 or later is required. The 3rd party ruamel.yaml package is
77+
Python 3.7 or later is required. The 3rd party ruamel.yaml package is
7878
also required. Note [_sleep-inhibitor_ is on
7979
PyPI](https://pypi.org/project/sleep-inhibitor/) so just ensure that
8080
`python3-pip` and `python3-wheel` are installed then type the following

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setup(
1313
name=name,
14-
version='1.16',
14+
version='1.17',
1515
description='Program to run plugins to inhibit system '
1616
'sleep/suspend/hibernate',
1717
long_description=here.joinpath('README.md').read_text(),
@@ -22,7 +22,7 @@
2222
keywords='bash',
2323
license='GPLv3',
2424
py_modules=[module],
25-
python_requires='>=3.6',
25+
python_requires='>=3.7',
2626
install_requires=['ruamel.yaml'],
2727
classifiers=[
2828
'Programming Language :: Python :: 3',

sleep-inhibitor.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# Default global check period, while ON/INHIBITING, in minutes. Can be
2323
# specified for each plugin, or if not specified will be the global
2424
# default you define here, or is "period" above if not specified at all.
25+
# Will be limited to "period" value so can not be larger.
2526
#
2627
# period_on:
2728
#

sleep_inhibitor.py

100644100755
Lines changed: 46 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
#!/usr/bin/python3
1+
#!/usr/bin/python3 -u
22
'Program to run plugins to inhibit system sleep/suspend.'
33
# Requires python 3.6+
44
# Mark Blakeney, Jul 2020.
55

6-
# Standard packages
7-
import sys
86
import argparse
7+
import asyncio
8+
import shlex
99
import subprocess
10-
import threading
10+
import sys
1111
import time
12-
import shlex
1312
from pathlib import Path
1413

1514
# Return code which indicates plugin wants to inhibit suspend
@@ -22,32 +21,26 @@
2221
'systemd-inhibit',
2322
)
2423

25-
def gettime(conf, field, default=None):
26-
'Read time value from given conf.'
27-
val = conf.get(field, default)
28-
if val is None:
29-
return None
30-
24+
def conv_to_secs(val):
25+
'Convert given time string to float seconds'
3126
if isinstance(val, str):
3227
if val.endswith('s'):
33-
num = float(val[:-1]) / 60
34-
elif val.endswith('m'):
3528
num = float(val[:-1])
36-
elif val.endswith('h'):
29+
elif val.endswith('m'):
3730
num = float(val[:-1]) * 60
31+
elif val.endswith('h'):
32+
num = float(val[:-1]) * 60 * 60
3833
else:
39-
sys.exit(f'Invalid time value "{field}: {val}".')
34+
sys.exit(f'Invalid time string "{val}".')
4035
else:
41-
num = float(val)
36+
# Default time entry is minutes
37+
num = float(val) * 60
4238

4339
return num
4440

4541
class Plugin:
4642
'Class to manage each plugin'
47-
loglock = threading.Lock()
48-
threads = []
49-
50-
def __init__(self, index, prog, progname, def_period, def_period_on,
43+
def __init__(self, index, prog, progname, period, period_on,
5144
def_what, conf, plugin_dir, inhibitor_prog):
5245
'Constructor'
5346
pathstr = conf.get('path')
@@ -69,15 +62,15 @@ def __init__(self, index, prog, progname, def_period, def_period_on,
6962
if not path.exists():
7063
sys.exit(f'{self.name}: "{path}" does not exist')
7164

72-
period = gettime(conf, 'period')
73-
if period is None:
74-
period = def_period
75-
period_on_def = def_period_on
76-
else:
77-
period_on_def = period
65+
period_str = conf.get('period', period)
66+
self.period = conv_to_secs(period_str)
67+
68+
period_on_str = conf.get('period_on', period_on)
69+
period_on = conv_to_secs(period_on_str)
70+
if period_on > self.period:
71+
period_on = self.period
72+
period_on_str = period_str
7873

79-
period_on = gettime(conf, 'period_on', period_on_def)
80-
self.period = period * 60
8174
self.is_inhibiting = None
8275

8376
cmd = str(path)
@@ -95,46 +88,31 @@ def __init__(self, index, prog, progname, def_period, def_period_on,
9588
# run the plugin in a loop which keeps the inhibit on while the
9689
# inhibit state is returned.
9790
self.icmd = shlex.split(f'{inhibitor_prog}{what} --who="{progname}" '
98-
f'--why="{self.name}" {prog} -s {period_on * 60} -i "{cmd}"')
91+
f'--why="{self.name}" {prog} -s {period_on} -i "{cmd}"')
9992

100-
per = round(period, 3)
101-
per_on = round(period_on, 3)
102-
print(f'{self.name} [{path}] configured @ {per}/{per_on} minutes')
93+
print(f'{self.name} [{path}] configured @ {period_str}/{period_on_str}')
10394

104-
# Each plugin periodic check runs in it's own thread
105-
thread = threading.Thread(target=self.run)
106-
thread.daemon = True
107-
thread.start()
108-
self.threads.append(thread)
109-
110-
def run(self):
111-
'Worker function which runs it its own thread'
95+
async def run(self):
96+
'Worker function which runs as a asyncio task for each plugin'
11297
while True:
113-
res = subprocess.run(self.cmd)
114-
while res.returncode == SUSP_CODE:
98+
proc = await asyncio.create_subprocess_exec(*self.cmd)
99+
await proc.wait()
100+
101+
while proc.returncode == SUSP_CODE:
115102
if not self.is_inhibiting:
116103
self.is_inhibiting = True
117-
self.log(f'{self.name} is inhibiting '
118-
f'suspend (return={res.returncode})')
104+
print(f'{self.name} is inhibiting '
105+
f'suspend (return={proc.returncode})')
119106

120-
res = subprocess.run(self.icmd)
107+
proc = await asyncio.create_subprocess_exec(*self.icmd)
108+
await proc.wait()
121109

122110
if self.is_inhibiting is not False:
123111
self.is_inhibiting = False
124-
self.log(f'{self.name} is not inhibiting '
125-
f'suspend (return={res.returncode})')
126-
127-
time.sleep(self.period)
128-
129-
@classmethod
130-
def log(cls, msg):
131-
'Thread locked print()'
132-
if not msg.endswith('\n'):
133-
msg += '\n'
112+
print(f'{self.name} is not inhibiting '
113+
f'suspend (return={proc.returncode})')
134114

135-
# Use a lock so thread messages do not get interleaved
136-
with cls.loglock:
137-
sys.stdout.write(msg)
115+
await asyncio.sleep(self.period)
138116

139117
def init():
140118
'Program initialisation'
@@ -214,22 +192,21 @@ def init():
214192
plugin_dir = args.plugin_dir or conf.get('plugin_dir', plugin_dir)
215193

216194
# Get some global defaults
217-
period = gettime(conf, 'period', 5)
218-
period_on = gettime(conf, 'period_on', period)
195+
period = conf.get('period', '5m')
196+
period_on = conf.get('period_on', period)
219197
what = conf.get('what')
220198

221199
# Iterate to create each configured plugins
222-
for index, plugin in enumerate(plugins, 1):
223-
Plugin(index, prog, progname, period, period_on, what, plugin,
224-
plugin_dir, inhibitor_prog)
200+
return [Plugin(index, prog, progname, period, period_on, what, plugin,
201+
plugin_dir, inhibitor_prog)
202+
for index, plugin in enumerate(plugins, 1)]
225203

226-
def main():
204+
async def main():
227205
'Main entry'
228-
init()
206+
tasks = init()
229207

230-
# Wait for each thread to finish (i.e. wait forever)
231-
for thread in Plugin.threads:
232-
thread.join()
208+
# Wait for each plugin task to finish (i.e. wait forever)
209+
await asyncio.gather(*(t.run() for t in tasks))
233210

234211
if __name__ == '__main__':
235-
sys.exit(main())
212+
asyncio.run(main(), debug=True)

test/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
1
2+
2
3+
3

test/run-test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
# Runs with a simple test configuration.
3+
# E.g. touch 1 2 3 to set all on, rm 1 2 3 to turn all off.
4+
../sleep_inhibitor.py -c test.conf -p $PWD | ts

test/test.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
period: 2s
2+
3+
plugins:
4+
- path: test.sh
5+
name: Test 1
6+
args: 1
7+
8+
- path: test.sh
9+
name: Test 2
10+
args: 2
11+
12+
- path: test.sh
13+
name: Test 3
14+
args: 3
15+
16+
# vim:se sw=2 syn=yaml et ai:

test/test.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
# Checks if jellyfin server is currently serving media to any user.
3+
# Mark Blakeney, Nov 2020.
4+
5+
TOKEN="$1"
6+
TDIR="$(dirname $0)"
7+
8+
if [[ -f $TDIR/$TOKEN ]]; then
9+
exit 254
10+
fi
11+
12+
exit 0

0 commit comments

Comments
 (0)