diff --git a/frappe_manager/docker_wrapper/DockerException.py b/frappe_manager/docker_wrapper/DockerException.py index 36ee121e..40bffe83 100644 --- a/frappe_manager/docker_wrapper/DockerException.py +++ b/frappe_manager/docker_wrapper/DockerException.py @@ -1,42 +1,34 @@ -from typing import List, Optional +from typing import List + +from frappe_manager.docker_wrapper.subprocess_output import SubprocessOutput + class DockerException(Exception): def __init__( self, command_launched: List[str], - return_code: int, - stdout: Optional[bytes] = None, - stderr: Optional[bytes] = None, + output: SubprocessOutput, ): self.docker_command: List[str] = command_launched - self.return_code: int = return_code - if stdout is None: - self.stdout: Optional[str] = None - else: - self.stdout: Optional[str] = stdout.decode() - if stderr is None: - self.stderr: Optional[str] = None - else: - self.stderr: Optional[str] = stderr.decode() + self.output = output + command_launched_str = " ".join(command_launched) + error_msg = ( f"The docker command executed was `{command_launched_str}`.\n" - f"It returned with code {return_code}\n" + f"It returned with code {self.output.exit_code}\n" ) - if stdout is not None: - error_msg += f"The content of stdout is '{self.stdout}'\n" + + if self.output.stdout: + error_msg += f"The content of stdout is '{self.output.stdout}'\n" else: - error_msg += ( - "The content of stdout can be found above the " - "stacktrace (it wasn't captured).\n" - ) - if stderr is not None: - error_msg += f"The content of stderr is '{self.stderr}'\n" + error_msg += "The content of stdout can be found above the " "stacktrace (it wasn't captured).\n" + + if self.output.stderr: + error_msg += f"The content of stderr is '{self.output.stderr}'\n" else: - error_msg += ( - "The content of stderr can be found above the " - "stacktrace (it wasn't captured)." - ) + error_msg += "The content of stderr can be found above the " "stacktrace (it wasn't captured)." + super().__init__(error_msg) diff --git a/frappe_manager/docker_wrapper/subprocess_output.py b/frappe_manager/docker_wrapper/subprocess_output.py new file mode 100644 index 00000000..8474a89e --- /dev/null +++ b/frappe_manager/docker_wrapper/subprocess_output.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class SubprocessOutput: + stdout: List[str] + stderr: List[str] + combined: List[str] + exit_code: int + + @classmethod + def from_output(cls, output): + stdout = [] + stderr = [] + combined = [] + exit_code = 0 + + for source, line in output: + line = line.decode() + if source == 'exit_code': + exit_code = int(line) + else: + combined.append(line) + if source == 'stdout': + stdout.append(line) + if source == 'stderr': + stderr.append(line) + + data = {'stdout': stdout, 'stderr': stderr, 'combined': combined, 'exit_code': exit_code} + return cls(**data) diff --git a/frappe_manager/services_manager/database_service_manager.py b/frappe_manager/services_manager/database_service_manager.py index ad6e6b4f..32d36132 100644 --- a/frappe_manager/services_manager/database_service_manager.py +++ b/frappe_manager/services_manager/database_service_manager.py @@ -17,7 +17,7 @@ from frappe_manager.display_manager.DisplayManager import richprint from pydantic import BaseModel -from frappe_manager.utils.docker import SubprocessOutput +from frappe_manager.docker_wrapper.subprocess_output import SubprocessOutput # TODO this class will be used for validation for main config diff --git a/frappe_manager/services_manager/services.py b/frappe_manager/services_manager/services.py index 6ad9ba95..605962ef 100644 --- a/frappe_manager/services_manager/services.py +++ b/frappe_manager/services_manager/services.py @@ -90,7 +90,6 @@ def init(self): self.fm_headers_path: Path = self.proxy_manager.dirs.confd.host / 'fm_headers.conf' self.set_frappe_headers_conf() - def set_frappe_headers_conf(self): if self.fm_headers_path.parent.exists(): template_path: Path = get_template_path('fm_headers.conf.tmpl') @@ -245,7 +244,7 @@ def shell(self, container: str, user: str | None = None): else: self.compose_project.docker.compose.exec(container, command=shell_path, capture_output=False) except DockerException as e: - richprint.warning(f"Shell exited with error code: {e.return_code}") + richprint.warning(f"Shell exited with error code: {e.output.exit_code}") def remove_itself(self): shutil.rmtree(self.path) diff --git a/frappe_manager/site_manager/site.py b/frappe_manager/site_manager/site.py index a2bd5c5a..9c5e6bb4 100644 --- a/frappe_manager/site_manager/site.py +++ b/frappe_manager/site_manager/site.py @@ -821,7 +821,7 @@ def shell(self, compose_service: str, user: str | None): try: self.compose_project.docker.compose.exec(**exec_args) except DockerException as e: - richprint.warning(f"Shell exited with error code: {e.return_code}") + richprint.warning(f"Shell exited with error code: {e.output.exit_code}") def get_log_file_paths(self): base_log_dir = self.path / "workspace" / "frappe-bench" / "logs" @@ -1197,9 +1197,11 @@ def is_supervisord_running(self, interval: int = 2, timeout: int = 30): for i in range(timeout): try: status_command = 'supervisorctl -c /opt/user/supervisord.conf status all' - self.frappe_service_run_command(status_command) + output = self.compose_project.docker.compose.exec('frappe', status_command, user='frappe', stream=False) return True - except BenchException: + except DockerException as e: + if any('frappe-bench' in s for s in e.output.combined): + return True time.sleep(interval) continue return False diff --git a/frappe_manager/utils/docker.py b/frappe_manager/utils/docker.py index fbd56a15..8b9dde99 100644 --- a/frappe_manager/utils/docker.py +++ b/frappe_manager/utils/docker.py @@ -3,44 +3,15 @@ from queue import Queue from subprocess import PIPE, Popen, run from threading import Thread -from dataclasses import dataclass -from typing import Dict, Iterable, List, Tuple, Union, Optional +from typing import Dict, Iterable, Tuple, Union, Optional from frappe_manager.logger import log from frappe_manager.docker_wrapper.DockerException import DockerException from frappe_manager.display_manager.DisplayManager import richprint +from frappe_manager.docker_wrapper.subprocess_output import SubprocessOutput process_opened = [] -@dataclass -class SubprocessOutput: - stdout: List[str] - stderr: List[str] - combined: List[str] - exit_code: int - - @classmethod - def from_output(cls, output): - stdout = [] - stderr = [] - combined = [] - exit_code = 0 - - for source, line in output: - line = line.decode() - if source == 'exit_code': - exit_code = int(line) - else: - combined.append(line) - if source == 'stdout': - stdout.append(line) - if source == 'stderr': - stderr.append(line) - - data = {'stdout': stdout, 'stderr': stderr, 'combined': combined, 'exit_code': exit_code} - return cls(**data) - - def reader(pipe, pipe_name, queue): """ Reads lines from a pipe and puts them into a queue. @@ -63,7 +34,7 @@ def reader(pipe, pipe_name, queue): def stream_stdout_and_stderr( full_cmd: list, - env: Dict[str, str] = None, + env: Optional[Dict[str, str]] = None, ) -> Iterable[Tuple[str, bytes]]: """ Executes a command in Docker and streams the stdout and stderr outputs. @@ -96,7 +67,7 @@ def stream_stdout_and_stderr( process_opened.append(process.pid) q = Queue() - full_stderr = b"" # for the error message + # we use deamon threads to avoid hanging if the user uses ctrl+c th = Thread(target=reader, args=[process.stdout, "stdout", q]) th.daemon = True @@ -105,21 +76,23 @@ def stream_stdout_and_stderr( th.daemon = True th.start() + output = [] for _ in range(2): for source, line in iter(q.get, None): + output.append((source, line)) yield source, line - if source == "stderr": - full_stderr += line exit_code = process.wait() logger.debug(f"RETURN CODE: {exit_code}") logger.debug('- -' * 10) - if exit_code != 0: - raise DockerException(full_cmd, exit_code, stderr=full_stderr) + output.append(('exit_code', str(exit_code).encode())) yield ("exit_code", str(exit_code).encode()) + if exit_code != 0: + raise DockerException(full_cmd, SubprocessOutput.from_output(output)) + def run_command_with_exit_code( full_cmd: list, @@ -140,7 +113,7 @@ def run_command_with_exit_code( run_output = run(full_cmd) exit_code = run_output.returncode if exit_code != 0: - raise DockerException(full_cmd, exit_code) + raise DockerException(full_cmd, SubprocessOutput([], [], [], exit_code)) return stream_output: SubprocessOutput = SubprocessOutput.from_output(stream_stdout_and_stderr(full_cmd)) diff --git a/pyproject.toml b/pyproject.toml index 73795de9..33e282c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "frappe-manager" -version = "0.13.3" +version = "0.13.4" license = "MIT" repository = "https://github.com/rtcamp/frappe-manager" description = "A CLI tool based on Docker Compose to easily manage Frappe based projects. As of now, only suitable for development in local machines running on Mac and Linux based OS."