From 6775d40e9243e7144566a296acfacea275322fb8 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 6 Jun 2026 20:13:51 +0200 Subject: reorganize files --- .dockerignore | 5 +- Dockerfile | 6 +-- app.py | 154 ------------------------------------------------------- html/index.html | 121 +++++++++++++++++++++++++++++++++++++++++++ index.html | 121 ------------------------------------------- server.py | 143 --------------------------------------------------- src/app.py | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/html | 1 + src/server.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test.sh | 4 +- 10 files changed, 438 insertions(+), 426 deletions(-) delete mode 100755 app.py create mode 100644 html/index.html delete mode 100644 index.html delete mode 100755 server.py create mode 100755 src/app.py create mode 120000 src/html create mode 100755 src/server.py diff --git a/.dockerignore b/.dockerignore index 069ef0d..d5aa556 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ * -!app.py -!index.html +!html/ !requirements.txt -!server.py +!src/ diff --git a/Dockerfile b/Dockerfile index e51f94d..af3cf63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,12 @@ FROM python:3.14-alpine RUN mkdir -p /var/lib/void -COPY [".", "/usr/src/"] -WORKDIR /usr/src +COPY [".", "/opt/void/"] +WORKDIR /opt/void RUN pip install -q -r requirements.txt # I don't know why, but in Docker, simple print() statements show nothing: # https://stackoverflow.com/q/29663459 # Fixed by adding the -u flag: -CMD ["python3", "-u", "./server.py", "--void", "/var/lib/void/void.state"] +CMD ["python3", "-u", "src/server.py", "--void", "/var/lib/void/void.state"] diff --git a/app.py b/app.py deleted file mode 100755 index 1655e00..0000000 --- a/app.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2022 Egor Tensin -# This file is part of the "void" project. -# For details, see https://github.com/egor-tensin/void. -# Distributed under the MIT License. - -import argparse -import cgi -from enum import Enum -import http.server -import os -import sys -from threading import Lock - - -class Response: - DEFAULT_STATUS = http.server.HTTPStatus.OK - - 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' - - def encode_body(self): - return self.body.encode(errors='replace') - - def write_as_cgi_script(self): - self.write_headers_as_cgi_script() - self.write_body_as_cgi_script() - - def write_headers_as_cgi_script(self): - print(f'{self.status.value} {self.status.phrase}') - for name, val in self.headers(): - print(f'{name}: {val}') - print() - - def write_body_as_cgi_script(self): - if self.body is not None: - print(self.body) - - 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_to_request_handler(self, handler): - for name, val in self.headers(): - handler.send_header(name, val) - handler.end_headers() - - def write_body_to_request_handler(self, handler): - if self.body is not None: - handler.wfile.write(self.encode_body()) - - -class Void: - def __init__(self, backup_path): - self.lck = Lock() - self.cnt = 0 - self.backup_path = backup_path - if backup_path is not None: - self.backup_path = os.path.abspath(backup_path) - - def __enter__(self): - if self.backup_path is not None and os.path.exists(self.backup_path): - self.restore(self.backup_path) - return self - - def __exit__(self, type, value, traceback): - if self.backup_path is not None: - os.makedirs(os.path.dirname(self.backup_path), exist_ok=True) - self.save(self.backup_path) - - def write(self, fd): - with self.lck: - cnt = self.cnt - fd.write(str(cnt)) - - def read(self, fd): - src = fd.read() - cnt = int(src) - with self.lck: - self.cnt = cnt - - def save(self, path): - with open(path, 'w', encoding='utf-8') as fd: - self.write(fd) - - def restore(self, path): - with open(path, encoding='utf-8') as fd: - self.read(fd) - - def scream_once(self): - with self.lck: - self.cnt += 1 - cnt = self.cnt - return Response(str(cnt)) - - def get_numof_screams(self): - with self.lck: - cnt = self.cnt - return Response(str(cnt)) - - -class Request(Enum): - SCREAM_ONCE = 'scream' - HOW_MANY_SCREAMS = 'screams' - - def __str__(self): - return self.value - - @staticmethod - def from_http_path(path): - if not path or path[0] != '/': - raise ValueError('HTTP path must start with a forward slash /') - return Request(path[1:]) - - def process(self, void): - if self is Request.SCREAM_ONCE: - return void.scream_once() - if self is Request.HOW_MANY_SCREAMS: - return void.get_numof_screams() - raise NotImplementedError(f'unknown request: {self}') - - -def process_cgi_request(void): - params = cgi.FieldStorage() - what = params['what'].value - Request(what).process(void).write_as_cgi_script() - - -def parse_args(args=None): - if args is None: - args = sys.argv[1:] - parser = argparse.ArgumentParser() - parser.add_argument( - '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' - ) - return parser.parse_args(args) - - -def main(args=None): - args = parse_args(args) - with Void(args.backup) as void: - process_cgi_request(void) - - -if __name__ == '__main__': - main() diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..124c7ea --- /dev/null +++ b/html/index.html @@ -0,0 +1,121 @@ + + + + + + + + + Орём! + + + + +
+
+
+
+
+

Пустота услышала

+

0 криков

+ +
+
+
+
+
+
+ + + + + + diff --git a/index.html b/index.html deleted file mode 100644 index 124c7ea..0000000 --- a/index.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - Орём! - - - - -
-
-
-
-
-

Пустота услышала

-

0 криков

- -
-
-
-
-
-
- - - - - - diff --git a/server.py b/server.py deleted file mode 100755 index 8c1470e..0000000 --- a/server.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2022 Egor Tensin -# This file is part of the "void" project. -# For details, see https://github.com/egor-tensin/void. -# Distributed under the MIT License. - -import argparse -from contextlib import contextmanager -import http.server -import os -import signal -import sys -from threading import Condition, Lock, Thread -import traceback - -from app import Request, Response, Void - -DEFAULT_PORT = 23666 -EXITING = False - - -def script_dir(): - return os.path.dirname(os.path.realpath(__file__)) - - -def set_exiting(): - global EXITING - with SignalHandler.LOCK: - EXITING = True - SignalHandler.CV.notify() - - -class SignalHandler: - LOCK = Lock() - CV = Condition(LOCK) - - def __init__(self, httpd): - self.httpd = httpd - self.thread = Thread(target=self.run) - - def __enter__(self): - self.thread.start() - return self - - def __exit__(self, type, value, traceback): - self.thread.join() - - def run(self): - with SignalHandler.CV: - while not EXITING: - SignalHandler.CV.wait() - self.httpd.shutdown() - - -def handle_sigterm(signum, frame): - print('\nSIGTERM received, exiting...') - set_exiting() - - -def handle_sigint(): - print('\nKeyboard interrupt received, exiting...') - set_exiting() - - -@contextmanager -def setup_signal_handlers(httpd): - handler_thread = SignalHandler(httpd) - with handler_thread: - signal.signal(signal.SIGTERM, handle_sigterm) - yield - # TODO: cleanup signal handlers? - - -class RequestHandler(http.server.SimpleHTTPRequestHandler): - VOID = None - - def address_string(self): - if 'x-forwarded-for' in self.headers: - return self.headers['x-forwarded-for'].split(',')[0].strip() - if 'x-real-ip' in self.headers: - return self.headers['x-real-ip'] - return super().address_string() - - def do_GET(self): - try: - request = Request.from_http_path(self.path) - except ValueError: - return super().do_GET() - try: - response = request.process(RequestHandler.VOID) - response.write_to_request_handler(self) - except: - status = http.server.HTTPStatus.INTERNAL_SERVER_ERROR - response = Response(traceback.format_exc(), status) - response.write_to_request_handler(self) - return - - -def make_server(port): - addr = ('', port) - server = http.server.HTTPServer - if sys.version_info >= (3, 7): - server = http.server.ThreadingHTTPServer - return server(addr, RequestHandler) - - -def parse_args(args=None): - if args is None: - args = sys.argv[1:] - parser = argparse.ArgumentParser() - parser.add_argument( - '-p', - '--port', - metavar='PORT', - type=int, - default=DEFAULT_PORT, - help='set port number', - ) - parser.add_argument( - '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' - ) - return parser.parse_args(args) - - -def main(args=None): - # It's a failsafe; this script is only allowed to serve the directory it - # resides in. - os.chdir(script_dir()) - - args = parse_args(args) - with Void(args.backup) as void: - RequestHandler.VOID = void - httpd = make_server(args.port) - with setup_signal_handlers(httpd): - try: - httpd.serve_forever() - except KeyboardInterrupt: - handle_sigint() - - -if __name__ == '__main__': - main() diff --git a/src/app.py b/src/app.py new file mode 100755 index 0000000..1655e00 --- /dev/null +++ b/src/app.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Egor Tensin +# This file is part of the "void" project. +# For details, see https://github.com/egor-tensin/void. +# Distributed under the MIT License. + +import argparse +import cgi +from enum import Enum +import http.server +import os +import sys +from threading import Lock + + +class Response: + DEFAULT_STATUS = http.server.HTTPStatus.OK + + 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' + + def encode_body(self): + return self.body.encode(errors='replace') + + def write_as_cgi_script(self): + self.write_headers_as_cgi_script() + self.write_body_as_cgi_script() + + def write_headers_as_cgi_script(self): + print(f'{self.status.value} {self.status.phrase}') + for name, val in self.headers(): + print(f'{name}: {val}') + print() + + def write_body_as_cgi_script(self): + if self.body is not None: + print(self.body) + + 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_to_request_handler(self, handler): + for name, val in self.headers(): + handler.send_header(name, val) + handler.end_headers() + + def write_body_to_request_handler(self, handler): + if self.body is not None: + handler.wfile.write(self.encode_body()) + + +class Void: + def __init__(self, backup_path): + self.lck = Lock() + self.cnt = 0 + self.backup_path = backup_path + if backup_path is not None: + self.backup_path = os.path.abspath(backup_path) + + def __enter__(self): + if self.backup_path is not None and os.path.exists(self.backup_path): + self.restore(self.backup_path) + return self + + def __exit__(self, type, value, traceback): + if self.backup_path is not None: + os.makedirs(os.path.dirname(self.backup_path), exist_ok=True) + self.save(self.backup_path) + + def write(self, fd): + with self.lck: + cnt = self.cnt + fd.write(str(cnt)) + + def read(self, fd): + src = fd.read() + cnt = int(src) + with self.lck: + self.cnt = cnt + + def save(self, path): + with open(path, 'w', encoding='utf-8') as fd: + self.write(fd) + + def restore(self, path): + with open(path, encoding='utf-8') as fd: + self.read(fd) + + def scream_once(self): + with self.lck: + self.cnt += 1 + cnt = self.cnt + return Response(str(cnt)) + + def get_numof_screams(self): + with self.lck: + cnt = self.cnt + return Response(str(cnt)) + + +class Request(Enum): + SCREAM_ONCE = 'scream' + HOW_MANY_SCREAMS = 'screams' + + def __str__(self): + return self.value + + @staticmethod + def from_http_path(path): + if not path or path[0] != '/': + raise ValueError('HTTP path must start with a forward slash /') + return Request(path[1:]) + + def process(self, void): + if self is Request.SCREAM_ONCE: + return void.scream_once() + if self is Request.HOW_MANY_SCREAMS: + return void.get_numof_screams() + raise NotImplementedError(f'unknown request: {self}') + + +def process_cgi_request(void): + params = cgi.FieldStorage() + what = params['what'].value + Request(what).process(void).write_as_cgi_script() + + +def parse_args(args=None): + if args is None: + args = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument( + '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' + ) + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + with Void(args.backup) as void: + process_cgi_request(void) + + +if __name__ == '__main__': + main() diff --git a/src/html b/src/html new file mode 120000 index 0000000..5eb7cf0 --- /dev/null +++ b/src/html @@ -0,0 +1 @@ +../html/ \ No newline at end of file diff --git a/src/server.py b/src/server.py new file mode 100755 index 0000000..0328d55 --- /dev/null +++ b/src/server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Egor Tensin +# This file is part of the "void" project. +# For details, see https://github.com/egor-tensin/void. +# Distributed under the MIT License. + +import argparse +from contextlib import contextmanager +import http.server +import os +import signal +import sys +from threading import Condition, Lock, Thread +import traceback + +from app import Request, Response, Void + +DEFAULT_PORT = 23666 +EXITING = False + + +def script_dir(): + return os.path.dirname(os.path.realpath(__file__)) + + +def default_html_dir(): + return os.path.join(script_dir(), 'html') + + +def set_exiting(): + global EXITING + with SignalHandler.LOCK: + EXITING = True + SignalHandler.CV.notify() + + +class SignalHandler: + LOCK = Lock() + CV = Condition(LOCK) + + def __init__(self, httpd): + self.httpd = httpd + self.thread = Thread(target=self.run) + + def __enter__(self): + self.thread.start() + return self + + def __exit__(self, type, value, traceback): + self.thread.join() + + def run(self): + with SignalHandler.CV: + while not EXITING: + SignalHandler.CV.wait() + self.httpd.shutdown() + + +def handle_sigterm(signum, frame): + print('\nSIGTERM received, exiting...') + set_exiting() + + +def handle_sigint(): + print('\nKeyboard interrupt received, exiting...') + set_exiting() + + +@contextmanager +def setup_signal_handlers(httpd): + handler_thread = SignalHandler(httpd) + with handler_thread: + signal.signal(signal.SIGTERM, handle_sigterm) + yield + # TODO: cleanup signal handlers? + + +class RequestHandler(http.server.SimpleHTTPRequestHandler): + VOID = None + + def address_string(self): + if 'x-forwarded-for' in self.headers: + return self.headers['x-forwarded-for'].split(',')[0].strip() + if 'x-real-ip' in self.headers: + return self.headers['x-real-ip'] + return super().address_string() + + def do_GET(self): + try: + request = Request.from_http_path(self.path) + except ValueError: + return super().do_GET() + try: + response = request.process(RequestHandler.VOID) + response.write_to_request_handler(self) + except: + status = http.server.HTTPStatus.INTERNAL_SERVER_ERROR + response = Response(traceback.format_exc(), status) + response.write_to_request_handler(self) + return + + +def make_server(port): + addr = ('', port) + server = http.server.HTTPServer + if sys.version_info >= (3, 7): + server = http.server.ThreadingHTTPServer + return server(addr, RequestHandler) + + +def parse_args(args=None): + if args is None: + args = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument( + '-p', + '--port', + metavar='PORT', + type=int, + default=DEFAULT_PORT, + help='set port number', + ) + parser.add_argument( + '-d', + '--dir', + metavar='DIR', + default=default_html_dir(), + help='HTML directory path', + ) + parser.add_argument( + '-v', '--void', metavar='PATH', dest='backup', help='set path to the void' + ) + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + + # It's a failsafe; this script is only allowed to serve the directory it + # resides in. + os.chdir(args.dir) + + with Void(args.backup) as void: + RequestHandler.VOID = void + httpd = make_server(args.port) + with setup_signal_handlers(httpd): + try: + httpd.serve_forever() + except KeyboardInterrupt: + handle_sigint() + + +if __name__ == '__main__': + main() diff --git a/test/test.sh b/test/test.sh index 9b1bc14..4b04350 100755 --- a/test/test.sh +++ b/test/test.sh @@ -28,7 +28,7 @@ dump() { run_server() { dump "Starting up server..." - "$script_dir/../server.py" --port "$server_port" & + "$script_dir/../src/server.py" --port "$server_port" & server_pid="$!" dump "Its PID is $server_pid" sleep 5 @@ -173,7 +173,7 @@ run_cgi_test() { local query_string="what=$what" dump "Running CGI test for query string: $query_string" - QUERY_STRING="$query_string" "$script_dir/../app.py" > "$curl_output_file" + QUERY_STRING="$query_string" "$script_dir/../src/app.py" > "$curl_output_file" cgi_check_header -- cgit v1.3