基于RPC构建控制守护进程的CLI客户端的实现问题咨询
问题根因
你遇到的两个问题本质是使用方式不符合Click和XMLRPC的设计逻辑:
AttributeError: 'Group' object has no attribute '__name__':被@click.group装饰后返回的是Click的Group类实例,并非原生函数,而XMLRPC注册函数要求传入的可调用对象必须带有__name__属性,因此直接注册会报错。- 输出打印在守护进程侧:Click默认会将命令的打印内容、返回值直接输出到运行进程的stdout/stderr,你在服务端直接调用Click组,输出自然只会留在服务端,不会自动序列化返回给客户端。
修复方案
方案1:业务逻辑与CLI解耦,CLI放在客户端实现(推荐)
不要直接注册Click的Group对象到RPC服务,将纯业务逻辑抽离单独注册,客户端用Click做参数解析,调用对应RPC方法拿到结果后本地打印。
服务端代码
######## Daemon ######## from xmlrpc.server import SimpleXMLRPCServer from xmlrpc.server import SimpleXMLRPCRequestHandler # 抽离纯业务逻辑,和Click完全解耦 def add(a, b): return int(a) + int(b) def subtract(a, b): return int(a) - int(b) class RequestHandler(SimpleXMLRPCRequestHandler): rpc_paths = ('/RPC2',) if __name__ == "__main__": with SimpleXMLRPCServer(('localhost', 8000), requestHandler=RequestHandler) as server: server.register_introspection_functions() # 直接注册业务函数 server.register_function(add) server.register_function(subtract) print("守护进程已启动,监听8000端口") server.serve_forever()
客户端代码
####### Client ######## import click import xmlrpc.client @click.group() def cli(): pass @cli.command() @click.argument("a") @click.argument("b") def add(a, b): with xmlrpc.client.ServerProxy("http://localhost:8000/") as proxy: res = proxy.add(a, b) # 结果在客户端本地打印 click.echo(f"计算结果:{res}") @cli.command() @click.argument("a") @click.argument("b") def subtract(a, b): with xmlrpc.client.ServerProxy("http://localhost:8000/") as proxy: res = proxy.subtract(a, b) click.echo(f"计算结果:{res}") if __name__ == "__main__": cli()
使用时直接在客户端侧执行python client.py add 1 2即可拿到输出,所有打印内容都留在客户端。
方案2:CLI定义放在服务端,封装调用捕获输出
如果希望统一维护CLI定义、避免客户端重复写参数规则,可以在服务端封装调用逻辑,捕获Click的输出后返回给客户端。
服务端封装示例
import io import click from contextlib import redirect_stdout, redirect_stderr from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler @click.group() def cli(): pass @cli.command() @click.argument("a") @click.argument("b") def add(a,b): click.echo(int(a) + int(b)) @cli.command() @click.argument("a") @click.argument("b") def subtract(a,b): click.echo(int(a) - int(b)) # 封装RPC调用入口,接收参数列表并返回执行结果 def run_cli(args): out_buffer = io.StringIO() err_buffer = io.StringIO() try: with redirect_stdout(out_buffer), redirect_stderr(err_buffer): cli.main(args=args) return {"code": 0, "stdout": out_buffer.getvalue(), "stderr": err_buffer.getvalue()} except SystemExit as e: return {"code": e.code, "stdout": out_buffer.getvalue(), "stderr": err_buffer.getvalue()} class RequestHandler(SimpleXMLRPCRequestHandler): rpc_paths = ('/RPC2',) if __name__ == "__main__": with SimpleXMLRPCServer(('localhost', 8000), requestHandler=RequestHandler) as server: server.register_introspection_functions() # 注册封装后的调用函数 server.register_function(run_cli) print("守护进程已启动,监听8000端口") server.serve_forever()
客户端调用示例
import sys import xmlrpc.client if __name__ == "__main__": # 将命令行参数直接传递给服务端 args = sys.argv[1:] with xmlrpc.client.ServerProxy("http://localhost:8000/") as proxy: res = proxy.run_cli(args) print(res["stdout"], end="") print(res["stderr"], end="") exit(res["code"])
这种方案不需要在客户端维护CLI规则,服务端更新命令后客户端无需修改即可直接使用。
内容的提问来源于stack exchange,提问作者sev
相关产品推荐
相关产品推荐

