feat: Enhance command execution handling in TerminalCog with subprocess improvements

This commit is contained in:
Slipstream 2025-05-17 19:09:42 -06:00
parent b598df1f82
commit 07cd5d9acf
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD

View File

@ -284,6 +284,12 @@ class TerminalCog(commands.Cog, name="Terminal"):
self.last_command = command # Store command for display after execution
# For other commands, use subprocess
if self.active_process and self.active_process.poll() is None:
self.output_history.append("A command is already running. Please wait or refresh.")
await self._update_terminal_message(interaction)
return
# For other commands, use subprocess
if self.active_process and self.active_process.poll() is None:
self.output_history.append("A command is already running. Please wait or refresh.")
@ -291,25 +297,35 @@ class TerminalCog(commands.Cog, name="Terminal"):
return
try:
# Use shlex.split for safer command parsing
command_parts = shlex.split(command)
if not command_parts:
self.output_history.append("No command provided.")
self.output_history.append(f"{self.current_cwd}> ")
self.scroll_offset = max(0, len(self.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self._update_terminal_message(interaction)
return
self.active_process = subprocess.Popen(
command,
shell=True, # Security risk: Be absolutely sure this is owner-only.
command_parts,
stdin=subprocess.PIPE, # Enable interactive input
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
text=True, # Use text mode for easier handling of output
cwd=self.current_cwd,
bufsize=1, # Line-buffered
universal_newlines=True # For text mode
# bufsize=1, # Removed line-buffering for better interactive handling
# universal_newlines=True # text=True handles this
)
if not self.auto_update_task.is_running():
self.auto_update_task.start()
# Initial update to show command is running
self.output_history.append(f"{self.current_cwd}> {command}") # Add command to history immediately
self.scroll_offset = max(0, len(self.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self._update_terminal_message(interaction)
except FileNotFoundError:
self.output_history.append(f"Error: Command not found: {command.split()[0]}")
self.output_history.append(f"Error: Command not found: {command_parts[0]}")
self.output_history.append(f"{self.current_cwd}> ")
self.scroll_offset = max(0, len(self.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self._update_terminal_message(interaction)
@ -330,23 +346,32 @@ class TerminalCog(commands.Cog, name="Terminal"):
updated = False
if self.active_process:
new_output_lines = []
interactive_prompt_detected = False
try:
# Read non-blockingly (or as much as possible without full block)
# stdout
while True: # Read all available lines from stdout
line = self.active_process.stdout.readline()
if not line: # No more output currently, or EOF
break
new_output_lines.append(line.strip()) # Strip newlines
updated = True
# stderr
while True: # Read all available lines from stderr
line = self.active_process.stderr.readline()
if not line:
break
new_output_lines.append(f"STDERR: {line.strip()}")
updated = True
# Read non-blockingly from stdout and stderr
while True:
stdout_line = self.active_process.stdout.readline()
stderr_line = self.active_process.stderr.readline()
if not stdout_line and not stderr_line:
break # No more output currently
if stdout_line:
line = stdout_line.strip()
new_output_lines.append(line)
updated = True
# Basic check for interactive prompts (can be improved)
if line.strip().endswith((':', '#', '$', '>')):
interactive_prompt_detected = True
if stderr_line:
line = f"STDERR: {stderr_line.strip()}"
new_output_lines.append(line)
updated = True
# Check stderr for prompts too, though less common
if stderr_line.strip().endswith((':', '#', '$', '>')):
interactive_prompt_detected = True
except Exception as e:
new_output_lines.append(f"Error reading process output: {e}")
@ -366,7 +391,7 @@ class TerminalCog(commands.Cog, name="Terminal"):
if final_stdout: self.output_history.extend(final_stdout.strip().splitlines())
if final_stderr: self.output_history.extend([f"STDERR: {l}" for l in final_stderr.strip().splitlines()])
self.output_history.append(f"Process finished with exit code {return_code}.")
self.output_history.append(f"{self.current_cwd}> ") # New prompt
self.active_process = None
@ -378,6 +403,11 @@ class TerminalCog(commands.Cog, name="Terminal"):
self.scroll_offset = max(0, len(self.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self._update_terminal_message(interaction)
# If an interactive prompt was detected and the process is still running,
# we might need to signal the user or change button states.
# This part will be handled in the TerminalView update_button_states
# and potentially a new mechanism for sending input.
class TerminalInputModal(ui.Modal, title="Send Command to Terminal"):
command_input = ui.TextInput(
@ -392,14 +422,31 @@ class TerminalInputModal(ui.Modal, title="Send Command to Terminal"):
self.cog = cog
async def on_submit(self, interaction: Interaction):
command = self.command_input.value
if command:
# Defer here as execute_shell_command can take time and will edit later
await interaction.response.defer()
await self.cog.execute_shell_command(command, interaction)
user_input = self.command_input.value
if not user_input:
await interaction.response.send_message("No input entered.", ephemeral=True)
return
# Defer the interaction as we will update the message later
await interaction.response.defer()
if self.cog.active_process and self.cog.active_process.poll() is None:
# There is an active process, assume the input is for it
try:
self.cog.active_process.stdin.write(user_input + '\n')
self.cog.active_process.stdin.flush()
# Add the input to history for display
self.cog.output_history.append(user_input)
self.cog.scroll_offset = max(0, len(self.cog.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self.cog._update_terminal_message(interaction) # Update message with the input
except Exception as e:
self.cog.output_history.append(f"Error sending input to process: {e}")
self.cog.scroll_offset = max(0, len(self.cog.output_history) - MAX_OUTPUT_LINES_PER_IMAGE)
await self.cog._update_terminal_message(interaction)
else:
await interaction.response.send_message("No command entered.", ephemeral=True)
# No active process, execute as a new command
await self.cog.execute_shell_command(user_input, interaction)
async def on_error(self, interaction: Interaction, error: Exception):
await interaction.response.send_message(f"Modal error: {error}", ephemeral=True)
print(f"TerminalInputModal error: {error}")