--- /dev/null
+#! /usr/bin/python3
+#
+# tailerd - run/tail commands as HTTP
+#
+# Copyright (c) 2020 Frederic Peters
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <http://www.gnu.org/licenses/>.
+#
+# ~~~~
+#
+# This script starts a webserver and will run commands specified in its
+# configuration file. It is tailored to run long-running commands such as
+# tail -f and journald -f.
+#
+# Requirements: aiohttp
+#
+# Configuration: ~/.config/tailerd.ini, example:
+#
+# [config]
+# command1 = /bin/journald -u whatever -f
+# command2 = /usr/bin/tail -f /var/log/whatever.log
+#
+# Commands that starts with a / will be run with exec() and arguments will be
+# split on spaces (no quoting). Commands that do not start with a / will be
+# passed to /bin/sh -c to be interpreted by the shell.
+#
+# An alternate location for the configuration file can be specified using the
+# -c/--config command line option.
+#
+# It runs on port 8080 and this can be changed using the -p/--port command line
+# option.
+
+import argparse
+import asyncio
+import configparser
+import os
+
+from aiohttp import web
+
+
+class Tailerd:
+ def __init__(self, config_filename):
+ self.config_filename = config_filename
+
+ async def handle(self, request):
+ config = configparser.ConfigParser()
+ config.read(os.path.join(os.path.expanduser(self.config_filename)))
+ try:
+ command = config.get('config', request.match_info['path'])
+ except (configparser.NoSectionError, configparser.NoOptionError):
+ return web.Response(status=404)
+ if command.startswith('/'):
+ command = command.split()
+ else:
+ command = ['/bin/sh', '-c', command]
+ response = web.StreamResponse(headers={'content-type': 'text/plain; charset=utf-8'})
+ response.enable_chunked_encoding()
+ await response.prepare(request)
+ process = await asyncio.create_subprocess_exec(
+ *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
+ )
+ try:
+ while True:
+ data = await process.stdout.readline()
+ await response.write(data)
+ if process.returncode is not None:
+ await response.write(b'-- End of command: %d\n' % process.returncode)
+ break
+ return response
+ except asyncio.CancelledError:
+ if process.returncode is None:
+ process.terminate()
+ raise
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c', '--config', dest='config', type=str, default='~/.config/tailerd.ini')
+ parser.add_argument('-p', '--port', dest='port', type=int, default=8080)
+ args = parser.parse_args()
+ app = web.Application()
+ app.add_routes([web.get('/{path}/', Tailerd(args.config).handle)])
+ web.run_app(app, port=args.port)