devservices 1.1.5__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.
- {devservices-1.1.5 → devservices-1.2.0}/PKG-INFO +1 -1
- {devservices-1.1.5 → devservices-1.2.0}/README.md +1 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/down.py +40 -1
- devservices-1.2.0/devservices/commands/foreground.py +132 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/list_services.py +9 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/logs.py +82 -6
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/reset.py +1 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/status.py +91 -7
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/up.py +173 -60
- {devservices-1.1.5 → devservices-1.2.0}/devservices/configs/service_config.py +42 -3
- {devservices-1.1.5 → devservices-1.2.0}/devservices/constants.py +8 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/main.py +2 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/dependencies.py +2 -10
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/docker_compose.py +49 -11
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/services.py +3 -3
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/state.py +2 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/supervisor.py +167 -3
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/SOURCES.txt +2 -0
- {devservices-1.1.5 → devservices-1.2.0}/pyproject.toml +1 -1
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_down.py +269 -0
- devservices-1.2.0/tests/commands/test_foreground.py +701 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_list_dependencies.py +7 -2
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_list_services.py +44 -0
- devservices-1.2.0/tests/commands/test_logs.py +738 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_status.py +341 -7
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_toggle.py +43 -5
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_up.py +604 -10
- {devservices-1.1.5 → devservices-1.2.0}/tests/configs/test_service_config.py +219 -12
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_dependencies.py +37 -1
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_services.py +5 -6
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_supervisor.py +304 -3
- devservices-1.1.5/tests/commands/test_logs.py +0 -223
- {devservices-1.1.5 → devservices-1.2.0}/LICENSE.md +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/__init__.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/__init__.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/purge.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/serve.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/toggle.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/commands/update.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/exceptions.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/__init__.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/console.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/devenv.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/docker.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/file_lock.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/git.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices/utils/install_binary.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/setup.cfg +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/testing/__init__.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/testing/utils.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/__init__.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_purge.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_reset.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_serve.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/commands/test_update.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/conftest.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_docker.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_git.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.1.5 → devservices-1.2.0}/tests/utils/test_state.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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"
|