mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
The web app (wsgi.py), novncd and socketiod set SIGCHLD to SIG_IGN at
import. The kernel then reaps every child at once, so subprocess loses
the exit status and a failing command reports 0, in the server and in
anything else run in that process: the test suite had to reset the
signal in two tests.
It was added in 2023 against one defunct bash per gunicorn worker and
per novncd. That bash came from the settings template of the time,
which ran .dec_ldap_pwd.sh with Popen and never waited for it; the
template stopped doing that in 561fedf. A settings.py still holding
that line leaves at most one zombie per process, not a growing number.
- wsgi.py, novncd, socketiod: the global SIG_IGN is removed. subprocess
reaps its own children, websockify installs its own SIGCHLD reaper
for novncd's connection processes, and novncd's SSH tunnels wait for
their ssh process.
- socketiod: the pty child execs consolecallback instead of running it
and then returning into the server's code; disconnect reaps that
child (waitpid) instead of any child (wait).
- The settings template drops its unused subprocess import; the two
test workarounds are removed.
New tests load the web app and novncd in a fresh interpreter and check
that SIGCHLD is untouched and a failing command still returns 1; both
fail on the previous code. A patched novncd in the test container
served five connections and left no child behind.
231 lines
5.9 KiB
Python
Executable file
231 lines
5.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import logging
|
|
import django
|
|
|
|
DIR_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT_PATH = os.path.abspath(os.path.join(DIR_PATH, "..", ""))
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings")
|
|
CERT = DIR_PATH + "/cert.pem"
|
|
|
|
if ROOT_PATH not in sys.path:
|
|
sys.path.append(ROOT_PATH)
|
|
|
|
django.setup()
|
|
|
|
from django.conf import settings as django_settings
|
|
|
|
# socketiod does not authenticate connections yet: any client
|
|
# that can reach the port can read and write an open serial console. Refuse
|
|
# to start unless an admin explicitly opts in.
|
|
if not getattr(django_settings, "SERIAL_CONSOLE_ENABLED", False):
|
|
sys.stderr.write(
|
|
"socketiod: the serial console is disabled (SERIAL_CONSOLE_ENABLED is not "
|
|
"True in settings). It is unauthenticated; only enable it on a trusted "
|
|
"network.\n"
|
|
)
|
|
sys.exit(1)
|
|
|
|
import re
|
|
import tempfile
|
|
import io
|
|
import socket
|
|
import socketio
|
|
import pty
|
|
import select
|
|
import struct
|
|
import fcntl
|
|
import termios
|
|
import signal
|
|
import eventlet
|
|
import atexit
|
|
import tty
|
|
import termios
|
|
import libvirt
|
|
|
|
# from six.moves import http_cookies as Cookie
|
|
from http import cookies as Cookie
|
|
from webvirtcloud.settings import SOCKETIO_PORT, SOCKETIO_HOST
|
|
from vrtManager.connection import CONN_SSH, CONN_SOCKET
|
|
import argparse
|
|
|
|
|
|
def get_parser(default_host=SOCKETIO_HOST, default_port=SOCKETIO_PORT or 6081):
|
|
parser = argparse.ArgumentParser(description="WebVirtCloud Socket.IO daemon")
|
|
parser.add_argument(
|
|
"-v",
|
|
"--verbose",
|
|
dest="verbose",
|
|
action="store_true",
|
|
help="Verbose mode",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"-d", "--debug", dest="debug", action="store_true", help="Debug mode", default=False
|
|
)
|
|
parser.add_argument(
|
|
"-H",
|
|
"--host",
|
|
dest="host",
|
|
action="store",
|
|
help="Listen host",
|
|
default=default_host,
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--port",
|
|
dest="port",
|
|
action="store",
|
|
type=int,
|
|
help="Listen port",
|
|
default=default_port,
|
|
)
|
|
return parser
|
|
|
|
|
|
options = get_parser().parse_args()
|
|
|
|
FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s"
|
|
if options.debug:
|
|
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
|
|
options.verbose = True
|
|
elif options.verbose:
|
|
logging.basicConfig(level=logging.INFO, format=FORMAT)
|
|
else:
|
|
logging.basicConfig(level=logging.WARNING, format=FORMAT)
|
|
|
|
async_mode = "eventlet"
|
|
sio = socketio.Server(async_mode=async_mode, cors_allowed_origins=[])
|
|
|
|
fd = None
|
|
child_pid = None
|
|
|
|
|
|
def get_connection_infos(token):
|
|
from instances.models import Instance
|
|
from vrtManager.instance import wvmInstance
|
|
|
|
try:
|
|
temptoken = token.split("-", 1)
|
|
host = int(temptoken[0])
|
|
uuid = temptoken[1]
|
|
instance = Instance.objects.get(compute_id=host, uuid=uuid)
|
|
conn = wvmInstance(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
instance.name,
|
|
)
|
|
except Exception as e:
|
|
logging.error(
|
|
"Fail to retrieve console connection infos for token %s : %s" % (token, e)
|
|
)
|
|
raise
|
|
return (instance, conn)
|
|
|
|
|
|
def set_winsize(fd, row, col, xpix=0, ypix=0):
|
|
winsize = struct.pack("HHHH", row, col, xpix, ypix)
|
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
|
|
|
|
|
def read_and_forward_pty_output():
|
|
global fd
|
|
max_read_bytes = 1024 * 20
|
|
while True:
|
|
sio.sleep(0.01)
|
|
if fd:
|
|
timeout_sec = 0
|
|
(data_ready, _, _) = select.select([fd], [], [], timeout_sec)
|
|
if data_ready:
|
|
output = os.read(fd, max_read_bytes).decode()
|
|
sio.emit("pty_output", {"output": output})
|
|
else:
|
|
return
|
|
|
|
|
|
@sio.event
|
|
def resize(sid, message):
|
|
global fd
|
|
if fd:
|
|
set_winsize(fd, message["rows"], message["cols"])
|
|
|
|
|
|
@sio.event
|
|
def pty_input(sid, message):
|
|
global fd
|
|
if fd:
|
|
os.write(fd, message["input"].encode())
|
|
|
|
|
|
@sio.event
|
|
def disconnect_request(sid):
|
|
sio.disconnect(sid)
|
|
|
|
|
|
@sio.event
|
|
def connect(sid, environ):
|
|
global fd
|
|
global child_pid
|
|
|
|
hcookie = environ.get("HTTP_COOKIE")
|
|
if hcookie:
|
|
cookie = Cookie.SimpleCookie()
|
|
for hcookie_part in hcookie.split(";"):
|
|
hcookie_part = hcookie_part.lstrip()
|
|
try:
|
|
cookie.load(hcookie_part)
|
|
except Cookie.CookieError:
|
|
logging.warn("Found malformed cookie")
|
|
else:
|
|
if "token" in cookie:
|
|
token = cookie["token"].value
|
|
|
|
if child_pid:
|
|
# already started child process, don't start another
|
|
# write a new line so that when a client refresh the shell prompt is printed
|
|
os.write(fd, str.encode("\n"))
|
|
return
|
|
|
|
# create child process attached to a pty we can read from and write to
|
|
(child_pid, fd) = pty.fork()
|
|
|
|
if child_pid == 0:
|
|
(instance, conn) = get_connection_infos(token)
|
|
uuid = conn.get_uuid()
|
|
uri = conn.wvm.getURI()
|
|
|
|
# Become the console process; returning would run this server's code
|
|
# in the child as well.
|
|
os.execv("conf/daemon/consolecallback", ["consolecallback", uri, uuid])
|
|
else:
|
|
# this is the parent process fork.
|
|
sio.start_background_task(target=read_and_forward_pty_output)
|
|
|
|
|
|
@sio.event
|
|
def disconnect(sid):
|
|
|
|
global fd
|
|
global child_pid
|
|
|
|
# kill pty process
|
|
try:
|
|
os.kill(child_pid, signal.SIGKILL)
|
|
os.waitpid(child_pid, 0) # only the child this daemon started
|
|
except ProcessLookupError:
|
|
pass
|
|
except ChildProcessError:
|
|
pass
|
|
|
|
# reset the variables
|
|
fd = None
|
|
child_pid = None
|
|
|
|
|
|
app = socketio.WSGIApp(sio)
|
|
import eventlet
|
|
|
|
eventlet.wsgi.server(eventlet.listen((options.host, int(options.port))), app)
|