|
2 | 2 | #
|
3 | 3 | # This file is part of Invenio.
|
4 | 4 | # Copyright (C) 2017-2018 CERN.
|
| 5 | +# Copyright (C) 2025 Graz University of Technology. |
5 | 6 | #
|
6 | 7 | # Invenio is free software; you can redistribute it and/or modify it
|
7 | 8 | # under the terms of the MIT License; see LICENSE file for more details.
|
8 | 9 |
|
9 | 10 | """CLI application for Invenio flavours."""
|
10 | 11 |
|
| 12 | +import contextlib |
| 13 | +import io |
| 14 | +import pickle |
| 15 | +import socket |
| 16 | +import socketserver |
| 17 | + |
| 18 | +from click import group, option, pass_context, secho |
| 19 | +from flask.cli import with_appcontext |
11 | 20 | from invenio_base.app import create_cli
|
12 | 21 |
|
13 | 22 | from .factory import create_app
|
14 | 23 |
|
15 | 24 | #: Invenio CLI application.
|
16 | 25 | cli = create_cli(create_app=create_app)
|
| 26 | + |
| 27 | + |
| 28 | +class RPCRequestHandler(socketserver.BaseRequestHandler): |
| 29 | + """RPCRequestHandler.""" |
| 30 | + |
| 31 | + def handle(self): |
| 32 | + """Handles the requests to the RPCServer.""" |
| 33 | + data = self.request.recv(4096) |
| 34 | + if not data: |
| 35 | + return |
| 36 | + |
| 37 | + try: |
| 38 | + command_parts = pickle.loads(data) |
| 39 | + |
| 40 | + if not isinstance(command_parts, list) or len(command_parts) == 0: |
| 41 | + raise ValueError("Invalid command format should be a list.") |
| 42 | + |
| 43 | + if command_parts[0] == "ping": |
| 44 | + result = "pong" |
| 45 | + else: |
| 46 | + output_buffer = io.StringIO() |
| 47 | + with contextlib.redirect_stdout(output_buffer): |
| 48 | + cli.main(args=command_parts, standalone_mode=False) |
| 49 | + result = output_buffer.getvalue().strip() |
| 50 | + |
| 51 | + response = pickle.dumps({"success": True, "result": result}) |
| 52 | + except Exception as e: |
| 53 | + response = pickle.dumps({"success": False, "error": str(e)}) |
| 54 | + |
| 55 | + self.request.sendall(response) |
| 56 | + |
| 57 | + |
| 58 | +class RPCServer(socketserver.TCPServer): |
| 59 | + """RPCServer implementation.""" |
| 60 | + |
| 61 | + allow_reuse_address = True |
| 62 | + |
| 63 | + def shutdown_server(self): |
| 64 | + """Shutdown the server.""" |
| 65 | + secho("Shutting down RPC Server...", fg="green") |
| 66 | + self.shutdown() |
| 67 | + self.server_close() |
| 68 | + |
| 69 | + |
| 70 | +def send(host, port, args): |
| 71 | + """Send.""" |
| 72 | + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| 73 | + s.connect((host, port)) |
| 74 | + s.sendall(pickle.dumps(list(args))) |
| 75 | + |
| 76 | + return pickle.loads(s.recv(4096)) |
| 77 | + |
| 78 | + |
| 79 | +@group() |
| 80 | +def rpc_server(): |
| 81 | + """RPC server.""" |
| 82 | + |
| 83 | + |
| 84 | +@rpc_server.command("start") |
| 85 | +@option("--port", default=5000) |
| 86 | +@option("--host", default="localhost") |
| 87 | +@with_appcontext |
| 88 | +def rpc_server_start(port, host): |
| 89 | + """Start rpc server.""" |
| 90 | + server = RPCServer((host, port), RPCRequestHandler) |
| 91 | + |
| 92 | + secho( |
| 93 | + f"RPC Server is running on port {host}:{port}... (Press Ctrl+C to stop)", |
| 94 | + fg="green", |
| 95 | + ) |
| 96 | + |
| 97 | + try: |
| 98 | + server.serve_forever() |
| 99 | + except KeyboardInterrupt: |
| 100 | + server.shutdown() |
| 101 | + |
| 102 | + |
| 103 | +@rpc_server.command( |
| 104 | + "send", |
| 105 | + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, |
| 106 | +) |
| 107 | +@option("--port", default=5000) |
| 108 | +@option("--host", default="localhost") |
| 109 | +@option("--plain", is_flag=True, default=False) |
| 110 | +@pass_context |
| 111 | +def rpc_server_send(ctx, port, host, plain): |
| 112 | + """Send.""" |
| 113 | + response = send(host, port, ctx.args) |
| 114 | + |
| 115 | + if response["success"]: |
| 116 | + prefix = "" if plain else "Response: " |
| 117 | + color = "green" |
| 118 | + message = response["result"] |
| 119 | + else: |
| 120 | + prefix = "" if plain else "Error: " |
| 121 | + color = "red" |
| 122 | + message = response["error"] |
| 123 | + |
| 124 | + secho(f"{prefix}{message}", fg=color) |
| 125 | + |
| 126 | + |
| 127 | +@rpc_server.command("ping") |
| 128 | +@option("--port", default=5000) |
| 129 | +@option("--host", default="localhost") |
| 130 | +def rpc_server_ping(port, host): |
| 131 | + """Ping.""" |
| 132 | + response = send(host, port, ["ping"]) |
| 133 | + secho(response["result"]) |
0 commit comments