Skip to content

Commit

Permalink
remove dependency on supervisorctl in favor of xmlrpc using the super…
Browse files Browse the repository at this point in the history
…visor unix socket
  • Loading branch information
psyb0t committed Sep 12, 2023
1 parent b50c74a commit e83e286
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 62 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ After installing, you can run the application with the following command:
supervisor-shell-ui
```

### Environment variables

- **SUPERVISOR_SOCK_PATH**: Defines the location of the supervisor sock file. Default is `/tmp/supervisor.sock`. If you're not sure where the sock file is try looking in the `supervisord.conf` usually located at `/etc/supervisor/supervisord.conf`.

### Keybindings

- **Esc**: Exit Page
Expand All @@ -99,7 +103,6 @@ supervisor-shell-ui

- This application is intended for use on UNIX-like operating systems.
- The application requires that the Supervisor program is already installed and properly configured on your system.
- supervisor-shell-ui relies on `supervisorctl` under the hood. In order to run it from a non-root user, you will need to set appropriate permissions for the `supervisor.sock` file in the supervisor configuration.

## License

Expand Down
8 changes: 6 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@

setup(
name='supervisor-shell-ui',
version='v0.2.7',
version='v0.3.0',
author='Ciprian Mandache',
author_email='psyb0t@51k.eu',
description='A CLI alternative to the built-in web interface of Supervisor, offering a more convenient way to manage processes directly from the terminal.',
long_description=long_description,
long_description_content_type="text/markdown",
python_requires='>=3.6',
packages=find_packages(),
install_requires=[],
install_requires=[
'urllib3==1.26.16',
'requests==2.31.0',
'requests-unixsocket==0.3.0',
],
entry_points={
'console_scripts': [
'supervisor-shell-ui=supervisor_shell_ui.main:main',
Expand Down
7 changes: 7 additions & 0 deletions supervisor_shell_ui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
LICENSE HEADER STOP"""
import os

SUPERVISOR_SOCK_PATH_ENV_VAR_NAME = "SUPERVISOR_SOCK_PATH"
APP_TITLE = "Supervisor Shell UI"
SUPERVISOR_SOCK_PATH = "/tmp/supervisor.sock"

if os.getenv(SUPERVISOR_SOCK_PATH_ENV_VAR_NAME):
SUPERVISOR_SOCK_PATH = os.getenv(SUPERVISOR_SOCK_PATH_ENV_VAR_NAME)
4 changes: 2 additions & 2 deletions supervisor_shell_ui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
import curses
import signal

from . import screen
from . import page_main
from supervisor_shell_ui import screen
from supervisor_shell_ui import page_main


def shutdown_handler(signal, frame):
Expand Down
8 changes: 4 additions & 4 deletions supervisor_shell_ui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
LICENSE HEADER STOP"""
from datetime import datetime

from . import common
from . import config
from . import screen
from . import keys
from supervisor_shell_ui import common
from supervisor_shell_ui import config
from supervisor_shell_ui import screen
from supervisor_shell_ui import keys

KEYBINDINGS_HELP = [
"Esc: Exit Page",
Expand Down
12 changes: 6 additions & 6 deletions supervisor_shell_ui/page_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
import curses

from datetime import datetime
from . import common
from . import supervisor
from . import keys
from . import screen
from . import page
from . import page_tail
from supervisor_shell_ui import common
from supervisor_shell_ui import supervisor
from supervisor_shell_ui import keys
from supervisor_shell_ui import screen
from supervisor_shell_ui import page
from supervisor_shell_ui import page_tail

SECTION_HEADER = 0
SECTION_PROCESS_TABLE = 1
Expand Down
10 changes: 5 additions & 5 deletions supervisor_shell_ui/page_tail.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
import time
import textwrap

from . import common
from . import keys
from . import supervisor
from . import screen
from . import page
from supervisor_shell_ui import common
from supervisor_shell_ui import keys
from supervisor_shell_ui import supervisor
from supervisor_shell_ui import screen
from supervisor_shell_ui import page

PAGE_TITLE = "Tail"
PAGE_BUTTON_REFRESH = 0
Expand Down
152 changes: 110 additions & 42 deletions supervisor_shell_ui/supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,74 +20,142 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
LICENSE HEADER STOP"""
import subprocess
import requests_unixsocket
import xml.etree.ElementTree as ET
import socket
from urllib.parse import quote
from supervisor_shell_ui import config

session = requests_unixsocket.Session()

LOG_SOURCE_STDOUT = "stdout"
LOG_SOURCE_STDERR = "stderr"


def exec(command, *args):
command_args = ['supervisorctl', command] + list(args)
def socket_exists(sock_path):
try:
output = subprocess.check_output(
command_args, stderr=subprocess.STDOUT).decode('utf-8')
return output.strip()
except subprocess.CalledProcessError as e:
error_message = e.output.decode('utf-8')
raise Exception(f"command failed with error: {error_message}")
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect(sock_path)
client.close()
return True
except Exception as e:
print(f"Couldn't connect to socket: {e}")
return False


def get_processes():
try:
output = exec('status')
except subprocess.CalledProcessError as e:
output = e.output.decode('utf-8')
exit_status = e.returncode
if exit_status != 3:
raise

lines = output.strip().split('\n')
def exec_rpc(method_name, *params):
if not socket_exists(config.SUPERVISOR_SOCK_PATH):
raise Exception(
f"Supervisor socket not found: {config.SUPERVISOR_SOCK_PATH}")

param_str = "".join(
[f"<param><value><string>{p}</string></value></param>" for p in params])

data = f"""<?xml version="1.0"?>
<methodCall>
<methodName>{method_name}</methodName>
<params>
{param_str}
</params>
</methodCall>
"""

url = f'http+unix://{quote(config.SUPERVISOR_SOCK_PATH, safe="")}/RPC2'
resp = session.post(url, data=data)
if resp.status_code != 200:
raise Exception(f"RPC call failed: {resp.text}")

return resp.text


def parse_process_info(struct_element):
process = {}
for member in struct_element.findall("member"):
name = member.find("name").text
value_type = list(member.find("value"))[0].tag
val = member.find(f"value/{value_type}").text
process[name] = int(val) if value_type == "int" else val

process["state"] = process["statename"]

return process


def parse_process_list_info(xml_data):
processes = []
for line in lines:
parts = line.split()
state = parts[1]
description = ' '.join(parts[2:]).replace(',', ' ')
name = parts[0]

processes.append({
"state": state,
"description": description,
"name": name,
})
for value in xml_data.findall(".//array/data/value/struct"):
process = parse_process_info(value)
processes.append(process)

return processes


def get_processes():
raw_data = exec_rpc("supervisor.getAllProcessInfo")
root = ET.fromstring(raw_data)
processes = parse_process_list_info(root)
processes = sorted(processes, key=lambda process: process["name"])

return processes


def tail_process_log(process_name, log_source, byte_count):
return exec('tail', f'-{byte_count}', process_name, log_source)
def get_process(name):
raw_data = exec_rpc("supervisor.getProcessInfo", name)
root = ET.fromstring(raw_data)
process = parse_process_info(root.find(".//struct"))

return process


def tail_process_log(name, log_source, byte_count):
rpc_method = "supervisor.tailProcessStdoutLog"
if log_source == LOG_SOURCE_STDERR:
rpc_method = "supervisor.tailProcessStderrLog"

raw_data = exec_rpc(rpc_method, name, 0, byte_count)
root = ET.fromstring(raw_data)
log_data = root.find(".//array/data/value/string").text

if log_data is None:
raise Exception(f'Process {name} has no {log_source} log')

return log_data


def restart_all():
return exec('restart', 'all')
processes = get_processes()
for process in processes:
restart_process(process["name"])

return "Restarted all processes."


def stop_all():
return exec('stop', 'all')
exec_rpc("supervisor.stopAllProcesses")

return "Stopped all processes."


def start_process(name):
exec_rpc("supervisor.startProcess", name)

return f"Started process {name}."


def stop_process(name):
exec_rpc("supervisor.stopProcess", name)

def start_process(process_name):
return exec('start', process_name)
return f"Stopped process {name}."


def restart_process(process_name):
return exec('restart', process_name)
def restart_process(name):
stop_process(name)
start_process(name)

return f"Restarted process {name}."

def stop_process(process_name):
return exec('stop', process_name)

def clear_process_log(name):
exec_rpc("supervisor.clearProcessLogs", name)

def clear_process_log(process_name):
return exec('clear', process_name)
return f"Cleared logs for process {name}."

0 comments on commit e83e286

Please sign in to comment.