Background shell data container. Pure data class that only stores state and output. IO operations are managed externally by BackgroundShellManager.
| 50 | |
| 51 | |
| 52 | class BackgroundShell: |
| 53 | """Background shell data container. |
| 54 | |
| 55 | Pure data class that only stores state and output. |
| 56 | IO operations are managed externally by BackgroundShellManager. |
| 57 | """ |
| 58 | |
| 59 | def __init__(self, bash_id: str, command: str, process: "asyncio.subprocess.Process", start_time: float): |
| 60 | self.bash_id = bash_id |
| 61 | self.command = command |
| 62 | self.process = process |
| 63 | self.start_time = start_time |
| 64 | self.output_lines: list[str] = [] |
| 65 | self.last_read_index = 0 |
| 66 | self.status = "running" |
| 67 | self.exit_code: int | None = None |
| 68 | |
| 69 | def add_output(self, line: str): |
| 70 | """Add new output line.""" |
| 71 | self.output_lines.append(line) |
| 72 | |
| 73 | def get_new_output(self, filter_pattern: str | None = None) -> list[str]: |
| 74 | """Get new output since last check, optionally filtered by regex.""" |
| 75 | new_lines = self.output_lines[self.last_read_index :] |
| 76 | self.last_read_index = len(self.output_lines) |
| 77 | |
| 78 | if filter_pattern: |
| 79 | try: |
| 80 | pattern = re.compile(filter_pattern) |
| 81 | new_lines = [line for line in new_lines if pattern.search(line)] |
| 82 | except re.error: |
| 83 | # Invalid regex, return all lines |
| 84 | pass |
| 85 | |
| 86 | return new_lines |
| 87 | |
| 88 | def update_status(self, is_alive: bool, exit_code: int | None = None): |
| 89 | """Update process status.""" |
| 90 | if not is_alive: |
| 91 | self.status = "completed" if exit_code == 0 else "failed" |
| 92 | self.exit_code = exit_code |
| 93 | else: |
| 94 | self.status = "running" |
| 95 | |
| 96 | async def terminate(self): |
| 97 | """Terminate the background process.""" |
| 98 | if self.process.returncode is None: |
| 99 | self.process.terminate() |
| 100 | try: |
| 101 | await asyncio.wait_for(self.process.wait(), timeout=5) |
| 102 | except asyncio.TimeoutError: |
| 103 | self.process.kill() |
| 104 | self.status = "terminated" |
| 105 | self.exit_code = self.process.returncode |
| 106 | |
| 107 | |
| 108 | class BackgroundShellManager: |