Private instance vars, public get properties

This commit is contained in:
2024-11-01 15:45:54 +01:00
parent d80171de15
commit a95c9676b4
18 changed files with 429 additions and 427 deletions

View File

@@ -1,6 +1,6 @@
from datetime import datetime
import subprocess
import xml.etree.ElementTree as ET
import xml.etree.ElementTree as ET
from typing import Optional
from .entry import Entry
@@ -10,13 +10,11 @@ class BackgroundEntry(Entry):
"""
Entry type for long-running background processes.
Attributes:
script: The script/command to execute
stdout: Captured standard output since last update
stderr: Captured standard error since last update
process: The running subprocess.Popen instance
_accumulated_stdout: Total stdout collected so far
_accumulated_stderr: Total stderr collected so far
Private Attributes:
_script: The script/command to execute
_stdout: Captured standard output
_stderr: Captured standard error
_process: The running subprocess.Popen instance
_exit_code: Exit code when process completes
"""
@@ -30,58 +28,73 @@ class BackgroundEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.script = script
self.stdout = ""
self.stderr = ""
self.process: Optional[subprocess.Popen] = None
self._accumulated_stdout = ""
self._accumulated_stderr = ""
self._script = script
self._stdout = ""
self._stderr = ""
self._process: Optional[subprocess.Popen] = None
self._exit_code: Optional[int] = None
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
@property
def pid(self) -> Optional[int]:
"""Get the process ID (None if not running)."""
return self._process.pid if self._process is not None else None
def cleanup(self) -> None:
"""
Clean up the background process if it's still running.
Ensures process is terminated and file handles are closed.
"""
self._cleanup_process()
def _cleanup_process(self):
"""Clean up process and file handles"""
if self.process is not None:
if self._process is not None:
try:
# Close file handles if they're open
if self.process.stdout:
self.process.stdout.close()
if self.process.stderr:
self.process.stderr.close()
if self._process.stdout:
self._process.stdout.close()
if self._process.stderr:
self._process.stderr.close()
# Terminate process if it's still running
if self.process.poll() is None:
self.process.terminate()
if self._process.poll() is None:
self._process.terminate()
try:
self.process.wait(timeout=1.0)
self._process.wait(timeout=1.0)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
self._process.kill()
self._process.wait()
except:
pass # Ignore cleanup errors
finally:
self.process = None
self._process = None
def update(self) -> None:
"""
Start the process if not running and collect any new output.
Updates stdout and stderr with any new output since last update.
Updates stdout and stderr with any new output.
"""
try:
# Reset current output buffers
self.stdout = ""
self.stderr = ""
# Start process if not running
if self.process is None and self._exit_code is None:
self.process = subprocess.Popen(
self.script,
if self._process is None and self._exit_code is None:
self._process = subprocess.Popen(
self._script,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -90,57 +103,52 @@ class BackgroundEntry(Entry):
)
return # Return after starting to allow process to generate output
if self.process is None:
if self._process is None:
return # Process already completed
# Check if process has finished
exit_code = self.process.poll()
exit_code = self._process.poll()
if exit_code is not None:
# Process has terminated, collect remaining output
try:
remaining_out, remaining_err = self.process.communicate(timeout=0.1)
self.stdout = remaining_out
self.stderr = remaining_err
self._accumulated_stdout += remaining_out
self._accumulated_stderr += remaining_err
remaining_out, remaining_err = self._process.communicate(timeout=0.1)
self._stdout += remaining_out
self._stderr += remaining_err
except subprocess.TimeoutExpired:
pass # Process didn't finish communicating, try again next update
self._exit_code = exit_code
self._cleanup_process()
self.cleanup()
return
# Read stdout if available
if self.process.stdout:
if self._process.stdout:
while True:
try:
line = self.process.stdout.readline()
line = self._process.stdout.readline()
if not line:
break
self.stdout += line
self._accumulated_stdout += line
self._stdout += line
except:
break
# Read stderr if available
if self.process.stderr:
if self._process.stderr:
while True:
try:
line = self.process.stderr.readline()
line = self._process.stderr.readline()
if not line:
break
self.stderr += line
self._accumulated_stderr += line
self._stderr += line
except:
break
except Exception as e:
# Handle any errors
error_msg = f"Error handling background process: {str(e)}"
self.stderr = error_msg
self._accumulated_stderr += f"\n{error_msg}"
self._stderr += f"\n{error_msg}"
self._exit_code = -1
self._cleanup_process()
self.cleanup()
def generate_context(self) -> ET.Element:
"""
@@ -150,26 +158,22 @@ class BackgroundEntry(Entry):
ET.Element: XML element containing the entry's data
"""
# Create root element with appropriate status
element = ET.Element("background", {"id": self.id})
element = ET.Element("background", {"id": self._id})
if self.process is not None:
element.set("pid", str(self.process.pid))
if self._process is not None:
element.set("pid", str(self._process.pid))
elif self._exit_code is not None:
element.set("exit_code", str(self._exit_code))
# Add script as CDATA or escaped text
element.text = escape_text_for_xml(self.script)
element.text = escape_text_for_xml(self._script)
# Add stdout element with accumulated output
stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self._accumulated_stdout)
stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element with accumulated output
stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self._accumulated_stderr)
stderr_elem.text = escape_text_for_xml(self._stderr)
return element
def __del__(self):
"""Ensure process is terminated and file handles are closed when entry is destroyed"""
self._cleanup_process()
return element