aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml2
-rwxr-xr-xapp.py72
-rw-r--r--index.html17
-rwxr-xr-xserver.py10
-rwxr-xr-xtest/test.sh8
5 files changed, 77 insertions, 32 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0e1d49..2fd7af1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,5 +19,7 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: '${{ matrix.python-version }}'
+ - name: Run tests as root
+ run: sudo ./test/test.sh
- name: Run tests
run: ./test/test.sh
diff --git a/app.py b/app.py
index 1ae9760..dd3edfc 100755
--- a/app.py
+++ b/app.py
@@ -70,18 +70,23 @@ def hostname():
class Response:
- def __init__(self, data):
- self.data = data
+ DEFAULT_STATUS = http.server.HTTPStatus.OK
+
+ @staticmethod
+ def body_from_json(body):
+ return json.dumps(body, ensure_ascii=False, indent=4)
+
+ def __init__(self, body, status=None):
+ if status is None:
+ status = Response.DEFAULT_STATUS
+ self.status = status
+ self.body = body
def headers(self):
yield 'Content-Type', 'text/html; charset=utf-8'
- @staticmethod
- def dump_json(data):
- return json.dumps(data, ensure_ascii=False, indent=4)
-
- def body(self):
- return self.dump_json(self.data)
+ def encode_body(self):
+ return self.body.encode(errors='replace')
def write_as_cgi_script(self):
self.write_headers_as_cgi_script()
@@ -93,26 +98,31 @@ class Response:
print()
def write_body_as_cgi_script(self):
- if self.data is not None:
- print(self.body())
+ if self.body is not None:
+ print(self.body)
- def write_as_request_handler(self, handler):
- handler.send_response(http.server.HTTPStatus.OK)
- self.write_headers_as_request_handler(handler)
- self.write_body_as_request_handler(handler)
+ def write_to_request_handler(self, handler):
+ handler.send_response(self.status)
+ self.write_headers_to_request_handler(handler)
+ self.write_body_to_request_handler(handler)
- def write_headers_as_request_handler(self, handler):
+ def write_headers_to_request_handler(self, handler):
for name, val in self.headers():
handler.send_header(name, val)
handler.end_headers()
- def write_body_as_request_handler(self, handler):
- if self.data is not None:
- handler.wfile.write(self.body().encode(errors='replace'))
+ def write_body_to_request_handler(self, handler):
+ if self.body is not None:
+ handler.wfile.write(self.encode_body())
def run_do(*args, **kwargs):
output = subprocess.run(args, stdin=DEVNULL, stdout=PIPE, stderr=STDOUT, universal_newlines=True, **kwargs)
+ # Include the output in the exception's message:
+ try:
+ output.check_returncode()
+ except Exception as e:
+ raise RuntimeError("Command's output was this:\n" + output.stdout) from e
return output.stdout
@@ -195,7 +205,7 @@ class Loginctl(Systemd):
class Task(abc.ABC):
def complete(self):
self.run()
- return Response(self.result())
+ return Response(Response.body_from_json(self.result()))
@abc.abstractmethod
def run(self):
@@ -301,7 +311,10 @@ class UserInstanceStatusTaskList(Task):
self.tasks = {user.name: UserInstanceStatusTask.su(user) for user in systemd_users()}
else:
# As a regular user, we can only query ourselves.
- self.tasks = {get_current_user().name: UserInstanceStatusTask()}
+ self.tasks = {}
+ user = get_current_user()
+ if user_instance_active(user):
+ self.tasks[user.name] = UserInstanceStatusTask()
def run(self):
for task in self.tasks.values():
@@ -353,6 +366,19 @@ def get_current_user():
return User(entry.pw_uid, entry.pw_name)
+def user_instance_active(user):
+ # I'm pretty sure this is the way to determine if the user instance is
+ # running?
+ # Source: https://www.freedesktop.org/software/systemd/man/user@.service.html
+ unit_name = f'user@{user.uid}.service'
+ cmd = Systemctl.system('is-active', unit_name, '--quiet')
+ try:
+ cmd.run().result()
+ return True
+ except Exception:
+ return False
+
+
# A pitiful attempt to find a list of possibly-systemd-enabled users follows
# (i.e. users that might be running a per-user systemd instance).
# I don't know of a better way than probing /run/user/UID.
@@ -394,16 +420,18 @@ def systemd_users():
for line in lines:
# This assumes user names cannot contain spaces.
# loginctl list-users output must be in the UID NAME format.
- info = line.split(' ', 2)
+ info = line.lstrip().split(' ', 2)
if len(info) < 2:
raise RuntimeError(f'invalid `loginctl list-users` output:\n{output}')
uid, user = info[0], info[1]
yield User(uid, user)
def show_users(users):
+ user_args = [user.name for user in users]
+ if not user_args:
+ return None
properties = 'UID', 'Name', 'RuntimePath'
prop_args = (arg for prop in properties for arg in ('-p', prop))
- user_args = (user.name for user in users)
output = Loginctl('show-user', *prop_args, '--value', *user_args).run().result()
lines = output.splitlines()
# Assuming that for muptiple users, the properties will be separated by
diff --git a/index.html b/index.html
index 52a45fc..e12adeb 100644
--- a/index.html
+++ b/index.html
@@ -64,12 +64,21 @@ h1, .h1 {
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script>
+function dump_fail(data) {
+ console.log('Response code was: ' + data.status + ' ' + data.statusText);
+ console.log('Response was:\n' + data.responseText);
+}
+
+function get(url, success_callback) {
+ $.get(url, success_callback).fail(dump_fail);
+}
+
function reboot() {
- $.get('reboot');
+ get('reboot');
}
function shutdown() {
- $.get('poweroff');
+ get('poweroff');
}
function set_hostname(data) {
@@ -82,7 +91,7 @@ function set_top(data) {
}
function refresh_top() {
- $.get('top', function(data) {
+ get('top', function(data) {
set_top(JSON.parse(data));
});
}
@@ -183,7 +192,7 @@ function set_users(data) {
}
function refresh_status() {
- $.get('status', function(data) {
+ get('status', function(data) {
data = JSON.parse(data);
set_hostname(data['hostname']);
set_system(data['system']);
diff --git a/server.py b/server.py
index 1b64766..0ebe5d5 100755
--- a/server.py
+++ b/server.py
@@ -12,8 +12,9 @@ import argparse
import http.server
import os
import sys
+import traceback
-from app import Request
+from app import Request, Response
DEFAULT_PORT = 18101
@@ -31,11 +32,12 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
return super().do_GET()
try:
response = request.process()
+ response.write_to_request_handler(self)
except:
- self.send_response(http.server.HTTPStatus.INTERNAL_SERVER_ERROR)
- self.end_headers()
+ status = http.server.HTTPStatus.INTERNAL_SERVER_ERROR
+ response = Response(traceback.format_exc(), status)
+ response.write_to_request_handler(self)
return
- response.write_as_request_handler(self)
def parse_args(args=None):
diff --git a/test/test.sh b/test/test.sh
index db44d5f..77a15e2 100755
--- a/test/test.sh
+++ b/test/test.sh
@@ -73,11 +73,10 @@ run_curl() {
local url="$1"
curl \
--silent --show-error \
- --fail \
--dump-header "$curl_header_file" \
--output "$curl_output_file" \
--connect-timeout 3 \
- -- "http://localhost:$server_port$url"
+ -- "http://localhost:$server_port$url" || true
}
curl_check_status() {
@@ -95,6 +94,11 @@ curl_check_status() {
dump "Actual HTTP response: $actual" >&2
dump "Expected: $expected" >&2
+
+ dump 'HTTP headers:' >&2
+ cat -- "$curl_header_file" >&2
+ dump 'HTTP response:' >&2
+ cat -- "$curl_output_file" >&2
return 1
}