Tanmoy Sarkar 88e8ef0f36 feat(companion): Add gunicorn-companion control CLI
Add a command-line client for the companion control socket so operators do not
have to hand-craft JSON. gunicorn.companion.ctl speaks the manager's
newline-delimited JSON protocol: status, start, stop, restart, and reread. The
socket path comes from --socket or . Exit status is 0
when the manager reports ok, 1 when it reports a failure, and 2 for a usage
error or an unreachable socket. Registered as the gunicorn-companion console
script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:22:41 +05:30

84 lines
2.5 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Command-line client for the companion control socket.
Speaks the newline-delimited JSON protocol the manager's ControlServer serves:
sends one command and prints the manager's reply. Installed as the
``gunicorn-companion`` console script.
"""
import argparse
import json
import os
import socket
import sys
# Commands that act on one named companion and so require a name argument.
PER_NAME_COMMANDS = ("start", "stop", "restart")
COMMANDS = ("status", "reread") + PER_NAME_COMMANDS
def send_command(socket_path, command):
"""Send one command dict to the control socket and return the reply dict."""
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
client.connect(socket_path)
client.sendall((json.dumps(command) + "\n").encode("utf-8"))
chunks = []
while True:
chunk = client.recv(65536)
if not chunk:
break
chunks.append(chunk)
if b"\n" in chunk:
break
finally:
client.close()
return json.loads(b"".join(chunks).decode("utf-8"))
def build_parser():
parser = argparse.ArgumentParser(
prog="gunicorn-companion",
description="Control gunicorn companion processes.")
parser.add_argument(
"-s", "--socket",
default=os.environ.get("GUNICORN_COMPANION_SOCKET"),
help="path to the companion control socket "
"(defaults to $GUNICORN_COMPANION_SOCKET)")
parser.add_argument("command", choices=COMMANDS)
parser.add_argument(
"name", nargs="?",
help="companion name (required for start, stop, restart)")
return parser
def run(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
if not args.socket:
parser.error("no control socket; pass --socket or set "
"GUNICORN_COMPANION_SOCKET")
if args.command in PER_NAME_COMMANDS and not args.name:
parser.error("%s requires a companion name" % args.command)
command = {"cmd": args.command}
if args.name:
command["name"] = args.name
try:
response = send_command(args.socket, command)
except OSError as error:
print("cannot reach companion socket %s: %s" % (args.socket, error),
file=sys.stderr)
return 2
print(json.dumps(response, indent=2))
return 0 if response.get("ok") else 1
if __name__ == "__main__":
sys.exit(run())