]> git.0d.be Git - tailerd.git/blob - tailerd.py
bind to localhost only
[tailerd.git] / tailerd.py
1 #! /usr/bin/python3
2 #
3 # tailerd - run/tail commands as HTTP
4 #
5 # Copyright (c) 2020 Frederic Peters
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, see <http://www.gnu.org/licenses/>.
19 #
20 # ~~~~
21 #
22 # This script starts a webserver and will run commands specified in its
23 # configuration file. It is tailored to run long-running commands such as
24 # tail -f and journald -f.
25 #
26 # Requirements: aiohttp
27 #
28 # Configuration: ~/.config/tailerd.ini, example:
29 #
30 #  [config]
31 #  command1 = /bin/journald -u whatever -f
32 #  command2 = /usr/bin/tail -f /var/log/whatever.log
33 #
34 # Commands that starts with a / will be run with exec() and arguments will be
35 # split on spaces (no quoting). Commands that do not start with a / will be
36 # passed to /bin/sh -c to be interpreted by the shell.
37 #
38 # An alternate location for the configuration file can be specified using the
39 # -c/--config command line option.
40 #
41 # It runs on port 8080 and this can be changed using the -p/--port command line
42 # option.
43
44 import argparse
45 import asyncio
46 import configparser
47 import os
48
49 from aiohttp import web
50
51
52 class Tailerd:
53     def __init__(self, config_filename):
54         self.config_filename = config_filename
55
56     async def handle(self, request):
57         config = configparser.ConfigParser()
58         config.read(os.path.join(os.path.expanduser(self.config_filename)))
59         try:
60             command = config.get('config', request.match_info['path'])
61         except (configparser.NoSectionError, configparser.NoOptionError):
62             return web.Response(status=404)
63         if command.startswith('/'):
64             command = command.split()
65         else:
66             command = ['/bin/sh', '-c', command]
67         response = web.StreamResponse(headers={'content-type': 'text/plain; charset=utf-8'})
68         response.enable_chunked_encoding()
69         await response.prepare(request)
70         process = await asyncio.create_subprocess_exec(
71             *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
72         )
73         try:
74             while True:
75                 data = await process.stdout.readline()
76                 await response.write(data)
77                 if process.returncode is not None:
78                     await response.write(b'-- End of command: %d\n' % process.returncode)
79                     break
80             return response
81         except asyncio.CancelledError:
82             if process.returncode is None:
83                 process.terminate()
84             raise
85
86
87 if __name__ == '__main__':
88     parser = argparse.ArgumentParser()
89     parser.add_argument('-c', '--config', dest='config', type=str, default='~/.config/tailerd.ini')
90     parser.add_argument('-p', '--port', dest='port', type=int, default=8080)
91     args = parser.parse_args()
92     app = web.Application()
93     app.add_routes([web.get('/{path}/', Tailerd(args.config).handle)])
94     web.run_app(app, host='127.0.0.1', port=args.port)