devservices 1.1.6__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {devservices-1.1.6 → devservices-1.2.0}/PKG-INFO +1 -1
  2. {devservices-1.1.6 → devservices-1.2.0}/README.md +1 -1
  3. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/down.py +40 -1
  4. devservices-1.2.0/devservices/commands/foreground.py +132 -0
  5. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/list_services.py +9 -1
  6. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/logs.py +82 -6
  7. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/reset.py +1 -1
  8. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/status.py +91 -7
  9. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/up.py +172 -60
  10. {devservices-1.1.6 → devservices-1.2.0}/devservices/configs/service_config.py +42 -3
  11. {devservices-1.1.6 → devservices-1.2.0}/devservices/constants.py +8 -0
  12. {devservices-1.1.6 → devservices-1.2.0}/devservices/main.py +2 -0
  13. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/dependencies.py +2 -10
  14. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/services.py +3 -3
  15. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/state.py +2 -1
  16. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/supervisor.py +167 -3
  17. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/PKG-INFO +1 -1
  18. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/SOURCES.txt +2 -0
  19. {devservices-1.1.6 → devservices-1.2.0}/pyproject.toml +1 -1
  20. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_down.py +269 -0
  21. devservices-1.2.0/tests/commands/test_foreground.py +701 -0
  22. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_list_dependencies.py +7 -2
  23. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_list_services.py +44 -0
  24. devservices-1.2.0/tests/commands/test_logs.py +738 -0
  25. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_status.py +341 -7
  26. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_toggle.py +43 -5
  27. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_up.py +389 -10
  28. {devservices-1.1.6 → devservices-1.2.0}/tests/configs/test_service_config.py +219 -12
  29. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_dependencies.py +37 -1
  30. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_services.py +5 -6
  31. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_supervisor.py +304 -3
  32. devservices-1.1.6/tests/commands/test_logs.py +0 -223
  33. {devservices-1.1.6 → devservices-1.2.0}/LICENSE.md +0 -0
  34. {devservices-1.1.6 → devservices-1.2.0}/devservices/__init__.py +0 -0
  35. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/__init__.py +0 -0
  36. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/list_dependencies.py +0 -0
  37. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/purge.py +0 -0
  38. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/serve.py +0 -0
  39. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/toggle.py +0 -0
  40. {devservices-1.1.6 → devservices-1.2.0}/devservices/commands/update.py +0 -0
  41. {devservices-1.1.6 → devservices-1.2.0}/devservices/exceptions.py +0 -0
  42. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/__init__.py +0 -0
  43. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/check_for_update.py +0 -0
  44. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/console.py +0 -0
  45. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/devenv.py +0 -0
  46. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/docker.py +0 -0
  47. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/docker_compose.py +0 -0
  48. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/file_lock.py +0 -0
  49. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/git.py +0 -0
  50. {devservices-1.1.6 → devservices-1.2.0}/devservices/utils/install_binary.py +0 -0
  51. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/dependency_links.txt +0 -0
  52. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/entry_points.txt +0 -0
  53. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/requires.txt +0 -0
  54. {devservices-1.1.6 → devservices-1.2.0}/devservices.egg-info/top_level.txt +0 -0
  55. {devservices-1.1.6 → devservices-1.2.0}/setup.cfg +0 -0
  56. {devservices-1.1.6 → devservices-1.2.0}/testing/__init__.py +0 -0
  57. {devservices-1.1.6 → devservices-1.2.0}/testing/utils.py +0 -0
  58. {devservices-1.1.6 → devservices-1.2.0}/tests/__init__.py +0 -0
  59. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_purge.py +0 -0
  60. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_reset.py +0 -0
  61. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_serve.py +0 -0
  62. {devservices-1.1.6 → devservices-1.2.0}/tests/commands/test_update.py +0 -0
  63. {devservices-1.1.6 → devservices-1.2.0}/tests/conftest.py +0 -0
  64. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_check_for_update.py +0 -0
  65. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_docker.py +0 -0
  66. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_docker_compose.py +0 -0
  67. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_git.py +0 -0
  68. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_install_binary.py +0 -0
  69. {devservices-1.1.6 → devservices-1.2.0}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.1.6
3
+ Version: 1.2.0
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -31,7 +31,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
31
31
  The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
32
32
 
33
33
  ```
34
- devservices==1.1.6
34
+ devservices==1.2.0
35
35
  ```
36
36
 
37
37
  ### 2. Add devservices config files
@@ -11,19 +11,21 @@ from sentry_sdk import capture_exception
11
11
 
12
12
  from devservices.constants import CONFIG_FILE_NAME
13
13
  from devservices.constants import DEPENDENCY_CONFIG_VERSION
14
+ from devservices.constants import DependencyType
14
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
16
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
17
  from devservices.constants import DEVSERVICES_DIR_NAME
18
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
17
19
  from devservices.exceptions import ConfigError
18
20
  from devservices.exceptions import ConfigNotFoundError
19
21
  from devservices.exceptions import DependencyError
20
22
  from devservices.exceptions import DockerComposeError
21
23
  from devservices.exceptions import ServiceNotFoundError
24
+ from devservices.exceptions import SupervisorError
22
25
  from devservices.utils.console import Console
23
26
  from devservices.utils.console import Status
24
27
  from devservices.utils.dependencies import construct_dependency_graph
25
28
  from devservices.utils.dependencies import DependencyNode
26
- from devservices.utils.dependencies import DependencyType
27
29
  from devservices.utils.dependencies import get_non_shared_remote_dependencies
28
30
  from devservices.utils.dependencies import install_and_verify_dependencies
29
31
  from devservices.utils.dependencies import InstalledRemoteDependency
@@ -35,6 +37,7 @@ from devservices.utils.services import Service
35
37
  from devservices.utils.state import ServiceRuntime
36
38
  from devservices.utils.state import State
37
39
  from devservices.utils.state import StateTables
40
+ from devservices.utils.supervisor import SupervisorManager
38
41
 
39
42
 
40
43
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -105,6 +108,14 @@ def down(args: Namespace) -> None:
105
108
  active_mode_dependencies = modes.get(active_mode, [])
106
109
  mode_dependencies.update(active_mode_dependencies)
107
110
 
111
+ supervisor_programs = [
112
+ dep
113
+ for dep in mode_dependencies
114
+ if dep in service.config.dependencies
115
+ and service.config.dependencies[dep].dependency_type
116
+ == DependencyType.SUPERVISOR
117
+ ]
118
+
108
119
  with Status(
109
120
  lambda: console.warning(f"Stopping {service.name}"),
110
121
  ) as status:
@@ -129,6 +140,13 @@ def down(args: Namespace) -> None:
129
140
  )
130
141
  exit(1)
131
142
 
143
+ try:
144
+ bring_down_supervisor_programs(supervisor_programs, service, status)
145
+ except SupervisorError as se:
146
+ capture_exception(se)
147
+ status.failure(str(se))
148
+ exit(1)
149
+
132
150
  # Check if any service depends on the service we are trying to bring down
133
151
  # TODO: We should also take into account the active modes of the other services (this is not trivial to do)
134
152
  other_started_services = active_services.difference({service.name})
@@ -285,3 +303,24 @@ def _bring_down_dependency(
285
303
  for dependency in cmd.services:
286
304
  status.info(f"Stopping {dependency}")
287
305
  return run_cmd(cmd.full_command, current_env)
306
+
307
+
308
+ def bring_down_supervisor_programs(
309
+ supervisor_programs: list[str], service: Service, status: Status
310
+ ) -> None:
311
+ if len(supervisor_programs) == 0:
312
+ return
313
+ programs_config_path = os.path.join(
314
+ service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
315
+ )
316
+ manager = SupervisorManager(
317
+ programs_config_path,
318
+ service_name=service.name,
319
+ )
320
+
321
+ for program in supervisor_programs:
322
+ status.info(f"Stopping {program}")
323
+ manager.stop_process(program)
324
+
325
+ status.info("Stopping supervisor daemon")
326
+ manager.stop_supervisor_daemon()
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import pty
5
+ import shlex
6
+ from argparse import _SubParsersAction
7
+ from argparse import ArgumentParser
8
+ from argparse import Namespace
9
+
10
+ from sentry_sdk import capture_exception
11
+
12
+ from devservices.constants import DependencyType
13
+ from devservices.constants import DEVSERVICES_DIR_NAME
14
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
15
+ from devservices.exceptions import ConfigError
16
+ from devservices.exceptions import ConfigNotFoundError
17
+ from devservices.exceptions import ServiceNotFoundError
18
+ from devservices.exceptions import SupervisorConfigError
19
+ from devservices.exceptions import SupervisorProcessError
20
+ from devservices.utils.console import Console
21
+ from devservices.utils.services import find_matching_service
22
+ from devservices.utils.state import State
23
+ from devservices.utils.state import StateTables
24
+ from devservices.utils.supervisor import SupervisorManager
25
+
26
+
27
+ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
28
+ parser = subparsers.add_parser(
29
+ "foreground", help="Run a service's program in the foreground"
30
+ )
31
+ parser.add_argument(
32
+ "program_name", help="Name of the program to run in the foreground"
33
+ )
34
+ parser.set_defaults(func=foreground)
35
+
36
+
37
+ def foreground(args: Namespace) -> None:
38
+ """Run a service's program in the foreground."""
39
+ console = Console()
40
+ program_name = args.program_name
41
+ try:
42
+ service = find_matching_service()
43
+ except ConfigNotFoundError as e:
44
+ capture_exception(e, level="info")
45
+ console.failure(
46
+ f"{str(e)}. Please specify a service (i.e. `devservices down sentry`) or run the command from a directory with a devservices configuration."
47
+ )
48
+ exit(1)
49
+ except ConfigError as e:
50
+ capture_exception(e)
51
+ console.failure(str(e))
52
+ exit(1)
53
+ except ServiceNotFoundError as e:
54
+ console.failure(str(e))
55
+ exit(1)
56
+ modes = service.config.modes
57
+ if program_name not in service.config.dependencies:
58
+ console.failure(
59
+ f"Program {program_name} does not exist in the service's config"
60
+ )
61
+ return
62
+ state = State()
63
+ starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
64
+ started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
65
+ active_services = starting_services.union(started_services)
66
+ if service.name not in active_services:
67
+ console.warning(f"{service.name} is not running")
68
+ return
69
+ active_starting_modes = state.get_active_modes_for_service(
70
+ service.name, StateTables.STARTING_SERVICES
71
+ )
72
+ active_started_modes = state.get_active_modes_for_service(
73
+ service.name, StateTables.STARTED_SERVICES
74
+ )
75
+ active_modes = active_starting_modes or active_started_modes
76
+ mode_dependencies = set()
77
+ for active_mode in active_modes:
78
+ active_mode_dependencies = modes.get(active_mode, [])
79
+ mode_dependencies.update(active_mode_dependencies)
80
+
81
+ supervisor_programs = [
82
+ dep
83
+ for dep in mode_dependencies
84
+ if dep in service.config.dependencies
85
+ and service.config.dependencies[dep].dependency_type
86
+ == DependencyType.SUPERVISOR
87
+ ]
88
+
89
+ if program_name not in supervisor_programs:
90
+ console.failure(
91
+ f"Program {program_name} is not running in any active modes of {service.name}"
92
+ )
93
+ return
94
+
95
+ programs_config_path = os.path.join(
96
+ service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
97
+ )
98
+
99
+ manager = SupervisorManager(
100
+ programs_config_path,
101
+ service_name=service.name,
102
+ )
103
+
104
+ try:
105
+ program_command = manager.get_program_command(program_name)
106
+ except SupervisorConfigError as e:
107
+ capture_exception(e, level="info")
108
+ console.failure(f"Error when getting program command: {str(e)}")
109
+ return
110
+
111
+ try:
112
+ # Stop the supervisor process before running in foreground
113
+ console.info(f"Stopping {program_name} in supervisor")
114
+ manager.stop_process(program_name)
115
+ console.info(f"Starting {program_name} in foreground")
116
+ argv = shlex.split(program_command)
117
+
118
+ # Run the process in foreground
119
+ pty.spawn(argv)
120
+ except SupervisorProcessError as e:
121
+ capture_exception(e)
122
+ console.failure(f"Error stopping {program_name} in supervisor: {str(e)}")
123
+ except (OSError, FileNotFoundError, PermissionError) as e:
124
+ capture_exception(e)
125
+ console.failure(f"Error running {program_name} in foreground: {str(e)}")
126
+
127
+ try:
128
+ console.info(f"Restarting {program_name} in background")
129
+ manager.start_process(program_name)
130
+ except SupervisorProcessError as e:
131
+ capture_exception(e)
132
+ console.failure(f"Error restarting {program_name} in background: {str(e)}")
@@ -4,6 +4,9 @@ from argparse import _SubParsersAction
4
4
  from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
+ from sentry_sdk import capture_exception
8
+
9
+ from devservices.exceptions import ConfigError
7
10
  from devservices.utils.console import Console
8
11
  from devservices.utils.devenv import get_coderoot
9
12
  from devservices.utils.services import get_local_services
@@ -29,7 +32,12 @@ def list_services(args: Namespace) -> None:
29
32
  console = Console()
30
33
  # Get all of the services installed locally
31
34
  coderoot = get_coderoot()
32
- services = get_local_services(coderoot)
35
+ try:
36
+ services = get_local_services(coderoot)
37
+ except ConfigError as e:
38
+ capture_exception(e)
39
+ console.failure(str(e))
40
+ return
33
41
  state = State()
34
42
  starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
35
43
  started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
@@ -11,15 +11,19 @@ from sentry_sdk import capture_exception
11
11
 
12
12
  from devservices.constants import CONFIG_FILE_NAME
13
13
  from devservices.constants import DEPENDENCY_CONFIG_VERSION
14
+ from devservices.constants import DependencyType
14
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
16
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
17
  from devservices.constants import DEVSERVICES_DIR_NAME
17
18
  from devservices.constants import MAX_LOG_LINES
19
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
18
20
  from devservices.exceptions import ConfigError
19
21
  from devservices.exceptions import ConfigNotFoundError
20
22
  from devservices.exceptions import DependencyError
21
23
  from devservices.exceptions import DockerComposeError
22
24
  from devservices.exceptions import ServiceNotFoundError
25
+ from devservices.exceptions import SupervisorConfigError
26
+ from devservices.exceptions import SupervisorError
23
27
  from devservices.utils.console import Console
24
28
  from devservices.utils.dependencies import install_and_verify_dependencies
25
29
  from devservices.utils.dependencies import InstalledRemoteDependency
@@ -29,6 +33,7 @@ from devservices.utils.services import find_matching_service
29
33
  from devservices.utils.services import Service
30
34
  from devservices.utils.state import State
31
35
  from devservices.utils.state import StateTables
36
+ from devservices.utils.supervisor import SupervisorManager
32
37
 
33
38
 
34
39
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -61,13 +66,25 @@ def logs(args: Namespace) -> None:
61
66
  except ServiceNotFoundError as e:
62
67
  console.failure(str(e))
63
68
  exit(1)
69
+ state = State()
64
70
 
65
71
  modes = service.config.modes
66
- # TODO: allow custom modes to be used
67
- mode_to_use = "default"
68
- mode_dependencies = modes[mode_to_use]
72
+ starting_modes = set(
73
+ state.get_active_modes_for_service(service.name, StateTables.STARTING_SERVICES)
74
+ )
75
+ started_modes = set(
76
+ state.get_active_modes_for_service(service.name, StateTables.STARTED_SERVICES)
77
+ )
78
+ active_modes = starting_modes.union(started_modes)
79
+ mode_dependencies = set()
80
+ for active_mode in active_modes:
81
+ active_mode_dependencies = modes.get(active_mode, [])
82
+ mode_dependencies.update(active_mode_dependencies)
83
+
84
+ # If no active modes found but service is running, fall back to default mode
85
+ if not mode_dependencies and "default" in modes:
86
+ mode_dependencies.update(modes["default"])
69
87
 
70
- state = State()
71
88
  starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
72
89
  started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
73
90
  running_services = starting_services.union(started_services)
@@ -76,7 +93,9 @@ def logs(args: Namespace) -> None:
76
93
  return
77
94
 
78
95
  try:
79
- remote_dependencies = install_and_verify_dependencies(service)
96
+ remote_dependencies = install_and_verify_dependencies(
97
+ service, modes=list(active_modes)
98
+ )
80
99
  except DependencyError as de:
81
100
  capture_exception(de)
82
101
  console.failure(
@@ -84,7 +103,7 @@ def logs(args: Namespace) -> None:
84
103
  )
85
104
  exit(1)
86
105
  try:
87
- logs_output = _logs(service, remote_dependencies, mode_dependencies)
106
+ logs_output = _logs(service, remote_dependencies, list(mode_dependencies))
88
107
  except DockerComposeError as dce:
89
108
  capture_exception(dce, level="info")
90
109
  console.failure(f"Failed to get logs for {service.name}: {dce.stderr}")
@@ -94,6 +113,22 @@ def logs(args: Namespace) -> None:
94
113
  if log_stdout is not None:
95
114
  console.info(log_stdout)
96
115
 
116
+ # Get supervisor program logs
117
+ supervisor_programs = [
118
+ dep
119
+ for dep in mode_dependencies
120
+ if dep in service.config.dependencies
121
+ and service.config.dependencies[dep].dependency_type
122
+ == DependencyType.SUPERVISOR
123
+ ]
124
+
125
+ if len(supervisor_programs) > 0:
126
+ supervisor_logs = _supervisor_logs(service, supervisor_programs)
127
+ for program_name, log_content in supervisor_logs.items():
128
+ if log_content:
129
+ console.info(f"=== Logs for supervisor program: {program_name} ===")
130
+ console.info(log_content)
131
+
97
132
 
98
133
  def _logs(
99
134
  service: Service,
@@ -133,3 +168,44 @@ def _logs(
133
168
  cmd_outputs.append(future.result())
134
169
 
135
170
  return cmd_outputs
171
+
172
+
173
+ def _supervisor_logs(
174
+ service: Service, supervisor_programs: list[str]
175
+ ) -> dict[str, str]:
176
+ if not supervisor_programs:
177
+ return {}
178
+
179
+ supervisor_logs: dict[str, str] = {}
180
+
181
+ programs_config_path = os.path.join(
182
+ service.repo_path, DEVSERVICES_DIR_NAME, PROGRAMS_CONF_FILE_NAME
183
+ )
184
+
185
+ try:
186
+ manager = SupervisorManager(programs_config_path, service_name=service.name)
187
+ except SupervisorConfigError as e:
188
+ capture_exception(e)
189
+ return supervisor_logs
190
+
191
+ with concurrent.futures.ThreadPoolExecutor() as executor:
192
+ futures = [
193
+ executor.submit(get_program_logs_with_error_handling, manager, program_name)
194
+ for program_name in supervisor_programs
195
+ ]
196
+ for future in concurrent.futures.as_completed(futures):
197
+ program_name, log_content = future.result()
198
+ supervisor_logs[program_name] = log_content
199
+
200
+ return supervisor_logs
201
+
202
+
203
+ def get_program_logs_with_error_handling(
204
+ manager: SupervisorManager, program_name: str
205
+ ) -> tuple[str, str]:
206
+ try:
207
+ log_content = manager.get_program_logs(program_name)
208
+ return program_name, log_content
209
+ except SupervisorError as e:
210
+ capture_exception(e)
211
+ return program_name, f"Error getting logs for {program_name}: {str(e)}"
@@ -7,6 +7,7 @@ from argparse import Namespace
7
7
  from sentry_sdk import capture_exception
8
8
 
9
9
  from devservices.commands.down import down
10
+ from devservices.constants import DependencyType
10
11
  from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11
12
  from devservices.exceptions import DockerDaemonNotRunningError
12
13
  from devservices.exceptions import DockerError
@@ -14,7 +15,6 @@ from devservices.utils.console import Console
14
15
  from devservices.utils.console import Status
15
16
  from devservices.utils.dependencies import construct_dependency_graph
16
17
  from devservices.utils.dependencies import DependencyNode
17
- from devservices.utils.dependencies import DependencyType
18
18
  from devservices.utils.docker import get_matching_containers
19
19
  from devservices.utils.docker import get_volumes_for_containers
20
20
  from devservices.utils.docker import remove_docker_resources
@@ -8,6 +8,7 @@ from argparse import _SubParsersAction
8
8
  from argparse import ArgumentParser
9
9
  from argparse import Namespace
10
10
  from collections import namedtuple
11
+ from datetime import timedelta
11
12
  from typing import TypedDict
12
13
 
13
14
  from sentry_sdk import capture_exception
@@ -15,9 +16,11 @@ from sentry_sdk import capture_exception
15
16
  from devservices.constants import Color
16
17
  from devservices.constants import CONFIG_FILE_NAME
17
18
  from devservices.constants import DEPENDENCY_CONFIG_VERSION
19
+ from devservices.constants import DependencyType
18
20
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
19
21
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
20
22
  from devservices.constants import DEVSERVICES_DIR_NAME
23
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
21
24
  from devservices.exceptions import ConfigError
22
25
  from devservices.exceptions import ConfigNotFoundError
23
26
  from devservices.exceptions import DependencyError
@@ -27,7 +30,6 @@ from devservices.utils.console import Console
27
30
  from devservices.utils.dependencies import construct_dependency_graph
28
31
  from devservices.utils.dependencies import DependencyGraph
29
32
  from devservices.utils.dependencies import DependencyNode
30
- from devservices.utils.dependencies import DependencyType
31
33
  from devservices.utils.dependencies import install_and_verify_dependencies
32
34
  from devservices.utils.dependencies import InstalledRemoteDependency
33
35
  from devservices.utils.docker_compose import get_docker_compose_commands_to_run
@@ -37,6 +39,8 @@ from devservices.utils.services import Service
37
39
  from devservices.utils.state import ServiceRuntime
38
40
  from devservices.utils.state import State
39
41
  from devservices.utils.state import StateTables
42
+ from devservices.utils.supervisor import ProcessInfo
43
+ from devservices.utils.supervisor import SupervisorManager
40
44
 
41
45
  BASE_INDENTATION = " "
42
46
 
@@ -99,8 +103,19 @@ def status(args: Namespace) -> None:
99
103
  console.warning(f"Status unavailable. {service.name} is not running standalone")
100
104
  return # Since exit(0) is captured as an internal_error by sentry
101
105
 
106
+ programs_config_path = os.path.join(
107
+ service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
108
+ )
109
+ process_statuses = {}
110
+ if os.path.exists(programs_config_path):
111
+ supervisor_manager = SupervisorManager(
112
+ programs_config_path,
113
+ service.name,
114
+ )
115
+ process_statuses = supervisor_manager.get_all_process_info()
116
+
102
117
  try:
103
- status_tree = get_status_for_service(service)
118
+ status_tree = get_status_for_service(service, process_statuses)
104
119
  except DependencyError as de:
105
120
  capture_exception(de)
106
121
  console.failure(
@@ -114,7 +129,9 @@ def status(args: Namespace) -> None:
114
129
  console.info(status_tree)
115
130
 
116
131
 
117
- def get_status_for_service(service: Service) -> str:
132
+ def get_status_for_service(
133
+ service: Service, process_statuses: dict[str, ProcessInfo]
134
+ ) -> str:
118
135
  state = State()
119
136
 
120
137
  modes = service.config.modes
@@ -141,7 +158,10 @@ def get_status_for_service(service: Service) -> str:
141
158
 
142
159
  docker_compose_service_to_status = parse_docker_compose_status(status_json_results)
143
160
  status_tree = generate_service_status_tree(
144
- service.name, dependency_graph, docker_compose_service_to_status
161
+ service.name,
162
+ process_statuses,
163
+ dependency_graph,
164
+ docker_compose_service_to_status,
145
165
  )
146
166
  return status_tree
147
167
 
@@ -188,6 +208,7 @@ def get_status_json_results(
188
208
 
189
209
  def generate_service_status_tree(
190
210
  service_name: str,
211
+ process_statuses: dict[str, ProcessInfo],
191
212
  dependency_graph: DependencyGraph,
192
213
  docker_compose_service_to_status: dict[str, ServiceStatusOutput],
193
214
  indentation: str = "",
@@ -227,6 +248,7 @@ def generate_service_status_tree(
227
248
  output.append(
228
249
  process_service_with_containerized_runtime(
229
250
  dependency,
251
+ process_statuses,
230
252
  docker_compose_service_to_status,
231
253
  indentation + BASE_INDENTATION,
232
254
  dependency_graph,
@@ -261,6 +283,7 @@ def process_service_with_local_runtime(
261
283
 
262
284
  def process_service_with_containerized_runtime(
263
285
  dependency: DependencyNode,
286
+ process_statuses: dict[str, ProcessInfo],
264
287
  docker_compose_service_to_status: dict[str, ServiceStatusOutput],
265
288
  indentation: str,
266
289
  dependency_graph: DependencyGraph,
@@ -268,13 +291,14 @@ def process_service_with_containerized_runtime(
268
291
  if len(dependency_graph.graph[dependency]) > 0:
269
292
  return generate_service_status_tree(
270
293
  dependency.name,
294
+ process_statuses,
271
295
  dependency_graph,
272
296
  docker_compose_service_to_status,
273
297
  indentation,
274
298
  )
275
299
  else:
276
300
  return generate_service_status_details(
277
- dependency, docker_compose_service_to_status, indentation
301
+ dependency, process_statuses, docker_compose_service_to_status, indentation
278
302
  )
279
303
 
280
304
 
@@ -301,16 +325,23 @@ def parse_docker_compose_status(
301
325
 
302
326
  def generate_service_status_details(
303
327
  dependency: DependencyNode,
328
+ process_statuses: dict[str, ProcessInfo],
304
329
  docker_compose_service_to_status: dict[str, ServiceStatusOutput],
305
330
  indentation: str,
306
331
  ) -> str:
307
332
  output = [f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:"]
308
333
 
334
+ # Handle supervisor dependencies
335
+ if dependency.dependency_type == DependencyType.SUPERVISOR:
336
+ return generate_supervisor_status_details(
337
+ dependency, process_statuses, indentation
338
+ )
339
+
309
340
  if dependency.name not in docker_compose_service_to_status:
310
341
  return "\n".join(
311
342
  [
312
343
  *output,
313
- f"{indentation}{BASE_INDENTATION}Type: container",
344
+ (f"{indentation}{BASE_INDENTATION}Type: container"),
314
345
  f"{indentation}{BASE_INDENTATION}Status: N/A",
315
346
  ]
316
347
  )
@@ -349,7 +380,7 @@ def handle_started_service(dependency: DependencyNode, indentation: str) -> str:
349
380
  f"{indentation}{BASE_INDENTATION}Runtime: local",
350
381
  ]
351
382
  )
352
- service_output = get_status_for_service(service_with_local_runtime)
383
+ service_output = get_status_for_service(service_with_local_runtime, {})
353
384
  return "\n".join(
354
385
  [f"{indentation}{line}" for line in service_output.splitlines()],
355
386
  )
@@ -365,3 +396,56 @@ def format_health(health: str) -> str:
365
396
  else Color.YELLOW
366
397
  )
367
398
  return f"{color}{health}{Color.RESET}"
399
+
400
+
401
+ def generate_supervisor_status_details(
402
+ dependency: DependencyNode,
403
+ process_statuses: dict[str, ProcessInfo],
404
+ indentation: str,
405
+ ) -> str:
406
+ """Generate status details for supervisor dependencies."""
407
+ output = [f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:"]
408
+
409
+ process_info = process_statuses.get(dependency.name)
410
+
411
+ if process_info is None:
412
+ return "\n".join(
413
+ [
414
+ *output,
415
+ f"{indentation}{BASE_INDENTATION}Type: process",
416
+ f"{indentation}{BASE_INDENTATION}Status: N/A (process not found)",
417
+ ]
418
+ )
419
+
420
+ uptime_str = format_uptime(process_info["uptime"])
421
+
422
+ details = [
423
+ "Type: process",
424
+ f"Status: {process_info['state_name'].lower()}",
425
+ f"PID: {process_info['pid'] if process_info['pid'] > 0 else 'N/A'}",
426
+ f"Uptime: {uptime_str}",
427
+ ]
428
+
429
+ output.extend(f"{indentation}{BASE_INDENTATION}{detail}" for detail in details)
430
+
431
+ return "\n".join(output)
432
+
433
+
434
+ def format_uptime(uptime_seconds: int) -> str:
435
+ """Format uptime seconds into a human-readable string."""
436
+ SECONDS_PER_MINUTE = 60
437
+ SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
438
+
439
+ td = timedelta(seconds=uptime_seconds)
440
+ days = td.days
441
+ hours, remainder = divmod(td.seconds, SECONDS_PER_HOUR)
442
+ minutes, seconds = divmod(remainder, SECONDS_PER_MINUTE)
443
+
444
+ if days > 0:
445
+ return f"{days}d {hours}h {minutes}m {seconds}s"
446
+ elif hours > 0:
447
+ return f"{hours}h {minutes}m {seconds}s"
448
+ elif minutes > 0:
449
+ return f"{minutes}m {seconds}s"
450
+ else:
451
+ return f"{seconds}s"