1
0
mirror of git://sourceware.org/git/lvm2.git synced 2024-12-21 13:34:40 +03:00

lvmdbusd: Use pseudo tty to get "lvm>" prompt again

When lvm is compiled with editline, if the file descriptors don't look like
a tty, then no "lvm> " prompt is done.  Having lvm output the shell prompt
when consuming JSON on a report file descriptor is very useful in
determining if lvm command is complete.
This commit is contained in:
Tony Asleson 2022-09-06 16:23:06 -05:00
parent 664a06650d
commit b3d13c50d7

108
daemons/lvmdbusd/lvm_shell_proxy.py.in Normal file → Executable file
View File

@ -14,11 +14,11 @@
import subprocess import subprocess
import shlex import shlex
import os import os
import pty
import sys import sys
import tempfile import tempfile
import time import time
import select import select
from .utils import extract_stack_trace
try: try:
import simplejson as json import simplejson as json
@ -28,7 +28,9 @@ except ImportError:
from lvmdbusd.cfg import LVM_CMD, run from lvmdbusd.cfg import LVM_CMD, run
from lvmdbusd.utils import log_debug, log_error, add_no_notify, make_non_block,\ from lvmdbusd.utils import log_debug, log_error, add_no_notify, make_non_block,\
read_decoded read_decoded, extract_stack_trace, LvmBug
SHELL_PROMPT = "lvm> "
def _quote_arg(arg): def _quote_arg(arg):
@ -44,7 +46,7 @@ class LVMShellProxy(object):
# up trying to get one. # up trying to get one.
# #
# Returns stdout, report (JSON), stderr # Returns stdout, report (JSON), stderr
def _read_response(self): def _read_response(self, no_output=False):
stdout = "" stdout = ""
report = "" report = ""
stderr = "" stderr = ""
@ -60,50 +62,49 @@ class LVMShellProxy(object):
while keep_reading and run.value != 0: while keep_reading and run.value != 0:
try: try:
rd_fd = [ rd_fd = [
self.lvm_shell.stdout.fileno(), self.parent_stdout_fd,
self.report_stream.fileno(), self.report_stream.fileno(),
self.lvm_shell.stderr.fileno()] self.parent_stderr_fd]
ready = select.select(rd_fd, [], [], 2) ready = select.select(rd_fd, [], [], 2)
for r in ready[0]: for r in ready[0]:
if r == self.lvm_shell.stdout.fileno(): if r == self.parent_stdout_fd:
stdout += read_decoded(self.lvm_shell.stdout) stdout += self.parent_stdout.readline()
elif r == self.report_stream.fileno(): elif r == self.report_stream.fileno():
report += read_decoded(self.report_stream) report += read_decoded(self.report_stream)
elif r == self.lvm_shell.stderr.fileno(): elif r == self.parent_stderr_fd:
stderr += read_decoded(self.lvm_shell.stderr) stderr += self.parent_stderr.readline()
# Check to see if the lvm process died on us # Check to see if the lvm process died on us
if self.lvm_shell.poll() is not None: if self.lvm_shell.poll() is not None:
raise Exception(self.lvm_shell.returncode, "%s" % stderr) raise Exception(self.lvm_shell.returncode, "%s" % stderr)
cur_report_len = len(report) if stdout.endswith(SHELL_PROMPT):
if cur_report_len != 0: if no_output:
# Only bother to parse if we have more data and the last 2 characters match expected keep_reading = False
# complete JSON, prevents excessive JSON parsing attempts else:
if prev_report_len != cur_report_len and report[-2:] == "}\n": cur_report_len = len(report)
prev_report_len = cur_report_len if cur_report_len != 0:
# Only bother to parse if we have more data
if prev_report_len != cur_report_len:
prev_report_len = cur_report_len
# Parse the JSON if it's good we are done,
# if not we will try to read some more.
try:
report_json = json.loads(report)
keep_reading = False
except ValueError:
pass
# Parse the JSON if it's good we are done, if keep_reading:
# if not we will try to read some more. extra_passes -= 1
try: if extra_passes <= 0:
report_json = json.loads(report) if len(report):
keep_reading = False raise LvmBug("Invalid json: %s" %
except ValueError: report)
pass else:
raise LvmBug(
# As long as lvm is spewing something on one of the FDs we will "lvm returned no JSON output!")
# keep trying. If we get a few timeouts with no activity, and
# we don't have valid JSON, we will raise an error.
if len(ready) == 0 and keep_reading:
extra_passes -= 1
if extra_passes <= 0:
if len(report):
raise ValueError("Invalid json: %s" %
report)
else:
raise ValueError(
"lvm returned no JSON output!")
except IOError as ioe: except IOError as ioe:
log_debug(str(ioe)) log_debug(str(ioe))
@ -119,10 +120,8 @@ class LVMShellProxy(object):
return stdout, report_json, stderr return stdout, report_json, stderr
def _write_cmd(self, cmd): def _write_cmd(self, cmd):
cmd_bytes = bytes(cmd, "utf-8") self.parent_stdin.write(cmd)
num_written = self.lvm_shell.stdin.write(cmd_bytes) self.parent_stdin.flush()
assert (num_written == len(cmd_bytes))
self.lvm_shell.stdin.flush()
def __init__(self): def __init__(self):
# Create a temp directory # Create a temp directory
@ -147,22 +146,37 @@ class LVMShellProxy(object):
if "LVM" in k: if "LVM" in k:
local_env[k] = v local_env[k] = v
self.parent_stdin_fd, child_stdin_fd = pty.openpty()
self.parent_stdout_fd, child_stdout_fd = pty.openpty()
self.parent_stderr_fd, child_stderr_fd = pty.openpty()
self.parent_stdin = os.fdopen(self.parent_stdin_fd, "w")
self.parent_stdout = os.fdopen(self.parent_stdout_fd, "r")
self.parent_stderr = os.fdopen(self.parent_stderr_fd, "r")
# run the lvm shell # run the lvm shell
self.lvm_shell = subprocess.Popen( self.lvm_shell = subprocess.Popen(
[LVM_CMD], [LVM_CMD],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=local_env, stdin=child_stdin_fd,
stderr=subprocess.PIPE, close_fds=True, pass_fds=(lvm_fd,), shell=False) stdout=child_stdout_fd, env=local_env,
stderr=child_stderr_fd, close_fds=True,
pass_fds=(lvm_fd,), shell=False)
try: try:
make_non_block(self.lvm_shell.stdout) make_non_block(self.parent_stdout_fd)
make_non_block(self.lvm_shell.stderr) make_non_block(self.parent_stderr_fd)
# Close our copy of the lvm_fd, child process is open in its process space # Close our copies of the child FDs there were created with the fork, we don't need them open.
os.close(lvm_fd) os.close(lvm_fd)
os.close(child_stdin_fd)
os.close(child_stdout_fd)
os.close(child_stderr_fd)
# Assume we are ready as we may not get the lvm prompt message depending on # wait for the first prompt
# if we are using readline or editline. log_debug("waiting for first prompt...")
errors = self._read_response(no_output=True)[2]
if errors and len(errors):
raise LvmBug(errors)
log_debug("lvm prompt read!!!")
except: except:
raise raise
finally: finally: