3 # tailerd - run/tail commands as HTTP
5 # Copyright (c) 2020 Frederic Peters
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.
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.
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/>.
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.
26 # Requirements: aiohttp
28 # Configuration: ~/.config/tailerd.ini, example:
31 # command1 = /bin/journald -u whatever -f
32 # command2 = /usr/bin/tail -f /var/log/whatever.log
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.
38 # An alternate location for the configuration file can be specified using the
39 # -c/--config command line option.
41 # It runs on port 8080 and this can be changed using the -p/--port command line
49 from aiohttp import web
53 def __init__(self, config_filename):
54 self.config_filename = config_filename
56 async def handle(self, request):
57 config = configparser.ConfigParser()
58 config.read(os.path.join(os.path.expanduser(self.config_filename)))
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()
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,
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)
81 except asyncio.CancelledError:
82 if process.returncode is None:
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)