devservices 1.1.6__tar.gz → 1.2.1__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.6 → devservices-1.2.1}/PKG-INFO +1 -1
- {devservices-1.1.6 → devservices-1.2.1}/README.md +3 -1
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/down.py +54 -1
- devservices-1.2.1/devservices/commands/foreground.py +133 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/list_services.py +9 -1
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/logs.py +80 -6
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/reset.py +1 -1
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/serve.py +17 -9
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/status.py +93 -7
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/up.py +185 -60
- {devservices-1.1.6 → devservices-1.2.1}/devservices/configs/service_config.py +46 -3
- {devservices-1.1.6 → devservices-1.2.1}/devservices/constants.py +8 -1
- {devservices-1.1.6 → devservices-1.2.1}/devservices/main.py +2 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/dependencies.py +2 -10
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/services.py +3 -3
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/state.py +2 -1
- devservices-1.2.1/devservices/utils/supervisor.py +433 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/SOURCES.txt +2 -0
- {devservices-1.1.6 → devservices-1.2.1}/pyproject.toml +1 -1
- {devservices-1.1.6 → devservices-1.2.1}/testing/utils.py +0 -9
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_down.py +234 -0
- devservices-1.2.1/tests/commands/test_foreground.py +665 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_list_dependencies.py +7 -2
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_list_services.py +44 -0
- devservices-1.2.1/tests/commands/test_logs.py +722 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_serve.py +14 -19
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_status.py +370 -35
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_toggle.py +43 -5
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_up.py +402 -10
- {devservices-1.1.6 → devservices-1.2.1}/tests/configs/test_service_config.py +254 -12
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_dependencies.py +37 -1
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_services.py +5 -6
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_supervisor.py +407 -13
- devservices-1.1.6/devservices/utils/supervisor.py +0 -208
- devservices-1.1.6/tests/commands/test_logs.py +0 -223
- {devservices-1.1.6 → devservices-1.2.1}/LICENSE.md +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/__init__.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/__init__.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/purge.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/toggle.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/commands/update.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/exceptions.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/__init__.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/console.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/devenv.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/docker.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/file_lock.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/git.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices/utils/install_binary.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/setup.cfg +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/testing/__init__.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/__init__.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_purge.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_reset.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/commands/test_update.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/conftest.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_docker.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_git.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.1.6 → devservices-1.2.1}/tests/utils/test_state.py +0 -0
|
@@ -23,6 +23,8 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
23
23
|
- `devservices update` Update devservices to the latest version.
|
|
24
24
|
- `devservices purge`: Purge the local devservices cache.
|
|
25
25
|
- `devservices toggle <service-name>`: Toggle the runtime for a service between containerized and local.
|
|
26
|
+
- `devservices serve`: Run a service's devserver.
|
|
27
|
+
- `devservices foreground <process-name>`: Foreground a process that is currently running in the background. This enables interactive debugging.
|
|
26
28
|
|
|
27
29
|
## Installation
|
|
28
30
|
|
|
@@ -31,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
31
33
|
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
34
|
|
|
33
35
|
```
|
|
34
|
-
devservices==1.1
|
|
36
|
+
devservices==1.2.1
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
### 2. Add devservices config files
|
|
@@ -11,6 +11,7 @@ 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
|
|
@@ -19,11 +20,12 @@ from devservices.exceptions import ConfigNotFoundError
|
|
|
19
20
|
from devservices.exceptions import DependencyError
|
|
20
21
|
from devservices.exceptions import DockerComposeError
|
|
21
22
|
from devservices.exceptions import ServiceNotFoundError
|
|
23
|
+
from devservices.exceptions import SupervisorConfigError
|
|
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,38 @@ 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 _stop_supervisor_program(
|
|
309
|
+
manager: SupervisorManager, program: str, status: Status
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Stop a single supervisor program."""
|
|
312
|
+
status.info(f"Stopping {program}")
|
|
313
|
+
manager.stop_process(program)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def bring_down_supervisor_programs(
|
|
317
|
+
supervisor_programs: list[str], service: Service, status: Status
|
|
318
|
+
) -> None:
|
|
319
|
+
if len(supervisor_programs) == 0:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
config_file_path = os.path.join(
|
|
323
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
328
|
+
except SupervisorConfigError:
|
|
329
|
+
raise
|
|
330
|
+
|
|
331
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
332
|
+
futures = [
|
|
333
|
+
executor.submit(_stop_supervisor_program, manager, program, status)
|
|
334
|
+
for program in supervisor_programs
|
|
335
|
+
]
|
|
336
|
+
for future in concurrent.futures.as_completed(futures):
|
|
337
|
+
_ = future.result()
|
|
338
|
+
|
|
339
|
+
status.info("Stopping supervisor daemon")
|
|
340
|
+
manager.stop_supervisor_daemon()
|
|
@@ -0,0 +1,133 @@
|
|
|
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 CONFIG_FILE_NAME
|
|
13
|
+
from devservices.constants import DependencyType
|
|
14
|
+
from devservices.constants import DEVSERVICES_DIR_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
|
+
config_file_path = os.path.join(
|
|
96
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
101
|
+
except SupervisorConfigError as e:
|
|
102
|
+
capture_exception(e, level="info")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
program_command = manager.get_program_command(program_name)
|
|
107
|
+
except SupervisorConfigError as e:
|
|
108
|
+
capture_exception(e, level="info")
|
|
109
|
+
console.failure(f"Error when getting program command: {str(e)}")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Stop the supervisor process before running in foreground
|
|
114
|
+
console.info(f"Stopping {program_name} in supervisor")
|
|
115
|
+
manager.stop_process(program_name)
|
|
116
|
+
console.info(f"Starting {program_name} in foreground")
|
|
117
|
+
argv = shlex.split(program_command)
|
|
118
|
+
|
|
119
|
+
# Run the process in foreground
|
|
120
|
+
pty.spawn(argv)
|
|
121
|
+
except SupervisorProcessError as e:
|
|
122
|
+
capture_exception(e)
|
|
123
|
+
console.failure(f"Error stopping {program_name} in supervisor: {str(e)}")
|
|
124
|
+
except (OSError, FileNotFoundError, PermissionError) as e:
|
|
125
|
+
capture_exception(e)
|
|
126
|
+
console.failure(f"Error running {program_name} in foreground: {str(e)}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
console.info(f"Restarting {program_name} in background")
|
|
130
|
+
manager.start_process(program_name)
|
|
131
|
+
except SupervisorProcessError as e:
|
|
132
|
+
capture_exception(e)
|
|
133
|
+
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,6 +11,7 @@ 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
|
|
@@ -20,6 +21,8 @@ from devservices.exceptions import ConfigNotFoundError
|
|
|
20
21
|
from devservices.exceptions import DependencyError
|
|
21
22
|
from devservices.exceptions import DockerComposeError
|
|
22
23
|
from devservices.exceptions import ServiceNotFoundError
|
|
24
|
+
from devservices.exceptions import SupervisorConfigError
|
|
25
|
+
from devservices.exceptions import SupervisorError
|
|
23
26
|
from devservices.utils.console import Console
|
|
24
27
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
25
28
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
@@ -29,6 +32,7 @@ from devservices.utils.services import find_matching_service
|
|
|
29
32
|
from devservices.utils.services import Service
|
|
30
33
|
from devservices.utils.state import State
|
|
31
34
|
from devservices.utils.state import StateTables
|
|
35
|
+
from devservices.utils.supervisor import SupervisorManager
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -61,13 +65,25 @@ def logs(args: Namespace) -> None:
|
|
|
61
65
|
except ServiceNotFoundError as e:
|
|
62
66
|
console.failure(str(e))
|
|
63
67
|
exit(1)
|
|
68
|
+
state = State()
|
|
64
69
|
|
|
65
70
|
modes = service.config.modes
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
starting_modes = set(
|
|
72
|
+
state.get_active_modes_for_service(service.name, StateTables.STARTING_SERVICES)
|
|
73
|
+
)
|
|
74
|
+
started_modes = set(
|
|
75
|
+
state.get_active_modes_for_service(service.name, StateTables.STARTED_SERVICES)
|
|
76
|
+
)
|
|
77
|
+
active_modes = starting_modes.union(started_modes)
|
|
78
|
+
mode_dependencies = set()
|
|
79
|
+
for active_mode in active_modes:
|
|
80
|
+
active_mode_dependencies = modes.get(active_mode, [])
|
|
81
|
+
mode_dependencies.update(active_mode_dependencies)
|
|
82
|
+
|
|
83
|
+
# If no active modes found but service is running, fall back to default mode
|
|
84
|
+
if not mode_dependencies and "default" in modes:
|
|
85
|
+
mode_dependencies.update(modes["default"])
|
|
69
86
|
|
|
70
|
-
state = State()
|
|
71
87
|
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
|
|
72
88
|
started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
|
|
73
89
|
running_services = starting_services.union(started_services)
|
|
@@ -76,7 +92,9 @@ def logs(args: Namespace) -> None:
|
|
|
76
92
|
return
|
|
77
93
|
|
|
78
94
|
try:
|
|
79
|
-
remote_dependencies = install_and_verify_dependencies(
|
|
95
|
+
remote_dependencies = install_and_verify_dependencies(
|
|
96
|
+
service, modes=list(active_modes)
|
|
97
|
+
)
|
|
80
98
|
except DependencyError as de:
|
|
81
99
|
capture_exception(de)
|
|
82
100
|
console.failure(
|
|
@@ -84,7 +102,7 @@ def logs(args: Namespace) -> None:
|
|
|
84
102
|
)
|
|
85
103
|
exit(1)
|
|
86
104
|
try:
|
|
87
|
-
logs_output = _logs(service, remote_dependencies, mode_dependencies)
|
|
105
|
+
logs_output = _logs(service, remote_dependencies, list(mode_dependencies))
|
|
88
106
|
except DockerComposeError as dce:
|
|
89
107
|
capture_exception(dce, level="info")
|
|
90
108
|
console.failure(f"Failed to get logs for {service.name}: {dce.stderr}")
|
|
@@ -94,6 +112,22 @@ def logs(args: Namespace) -> None:
|
|
|
94
112
|
if log_stdout is not None:
|
|
95
113
|
console.info(log_stdout)
|
|
96
114
|
|
|
115
|
+
# Get supervisor program logs
|
|
116
|
+
supervisor_programs = [
|
|
117
|
+
dep
|
|
118
|
+
for dep in mode_dependencies
|
|
119
|
+
if dep in service.config.dependencies
|
|
120
|
+
and service.config.dependencies[dep].dependency_type
|
|
121
|
+
== DependencyType.SUPERVISOR
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
if len(supervisor_programs) > 0:
|
|
125
|
+
supervisor_logs = _supervisor_logs(service, supervisor_programs)
|
|
126
|
+
for program_name, log_content in supervisor_logs.items():
|
|
127
|
+
if log_content:
|
|
128
|
+
console.info(f"=== Logs for supervisor program: {program_name} ===")
|
|
129
|
+
console.info(log_content)
|
|
130
|
+
|
|
97
131
|
|
|
98
132
|
def _logs(
|
|
99
133
|
service: Service,
|
|
@@ -133,3 +167,43 @@ def _logs(
|
|
|
133
167
|
cmd_outputs.append(future.result())
|
|
134
168
|
|
|
135
169
|
return cmd_outputs
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _supervisor_logs(
|
|
173
|
+
service: Service, supervisor_programs: list[str]
|
|
174
|
+
) -> dict[str, str]:
|
|
175
|
+
if not supervisor_programs:
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
supervisor_logs: dict[str, str] = {}
|
|
179
|
+
|
|
180
|
+
config_file_path = os.path.join(
|
|
181
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
185
|
+
except SupervisorConfigError as e:
|
|
186
|
+
capture_exception(e)
|
|
187
|
+
return supervisor_logs
|
|
188
|
+
|
|
189
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
190
|
+
futures = [
|
|
191
|
+
executor.submit(get_program_logs_with_error_handling, manager, program_name)
|
|
192
|
+
for program_name in supervisor_programs
|
|
193
|
+
]
|
|
194
|
+
for future in concurrent.futures.as_completed(futures):
|
|
195
|
+
program_name, log_content = future.result()
|
|
196
|
+
supervisor_logs[program_name] = log_content
|
|
197
|
+
|
|
198
|
+
return supervisor_logs
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_program_logs_with_error_handling(
|
|
202
|
+
manager: SupervisorManager, program_name: str
|
|
203
|
+
) -> tuple[str, str]:
|
|
204
|
+
try:
|
|
205
|
+
log_content = manager.get_program_logs(program_name)
|
|
206
|
+
return program_name, log_content
|
|
207
|
+
except SupervisorError as e:
|
|
208
|
+
capture_exception(e)
|
|
209
|
+
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
|
|
@@ -9,8 +9,8 @@ from argparse import Namespace
|
|
|
9
9
|
|
|
10
10
|
from sentry_sdk import capture_exception
|
|
11
11
|
|
|
12
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
12
13
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
13
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
14
14
|
from devservices.exceptions import ConfigError
|
|
15
15
|
from devservices.exceptions import ConfigNotFoundError
|
|
16
16
|
from devservices.exceptions import SupervisorConfigError
|
|
@@ -46,16 +46,24 @@ def serve(args: Namespace) -> None:
|
|
|
46
46
|
console.failure(str(e))
|
|
47
47
|
exit(1)
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
service.repo_path,
|
|
49
|
+
config_file_path = os.path.join(
|
|
50
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
51
51
|
)
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
55
|
+
except SupervisorConfigError as e:
|
|
56
|
+
capture_exception(e, level="info")
|
|
57
|
+
console.failure(
|
|
58
|
+
f"Unable to bring up devserver due to supervisor config error: {str(e)}"
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if not manager.has_programs:
|
|
63
|
+
console.failure(
|
|
64
|
+
"No programs found in config. Please add the devserver in the `x-programs` block to your config.yml"
|
|
65
|
+
)
|
|
54
66
|
return
|
|
55
|
-
manager = SupervisorManager(
|
|
56
|
-
programs_config_path,
|
|
57
|
-
service_name=service.name,
|
|
58
|
-
)
|
|
59
67
|
|
|
60
68
|
try:
|
|
61
69
|
devserver_command = manager.get_program_command("devserver")
|
|
@@ -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,6 +16,7 @@ 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,11 +25,11 @@ from devservices.exceptions import ConfigNotFoundError
|
|
|
23
25
|
from devservices.exceptions import DependencyError
|
|
24
26
|
from devservices.exceptions import DockerComposeError
|
|
25
27
|
from devservices.exceptions import ServiceNotFoundError
|
|
28
|
+
from devservices.exceptions import SupervisorConfigError
|
|
26
29
|
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,21 @@ 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
|
+
config_file_path = os.path.join(
|
|
107
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
108
|
+
)
|
|
109
|
+
process_statuses = {}
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
supervisor_manager = SupervisorManager(service.name, config_file_path)
|
|
113
|
+
process_statuses = supervisor_manager.get_all_process_info()
|
|
114
|
+
except SupervisorConfigError as e:
|
|
115
|
+
capture_exception(e)
|
|
116
|
+
console.failure(str(e))
|
|
117
|
+
exit(1)
|
|
118
|
+
|
|
102
119
|
try:
|
|
103
|
-
status_tree = get_status_for_service(service)
|
|
120
|
+
status_tree = get_status_for_service(service, process_statuses)
|
|
104
121
|
except DependencyError as de:
|
|
105
122
|
capture_exception(de)
|
|
106
123
|
console.failure(
|
|
@@ -114,7 +131,9 @@ def status(args: Namespace) -> None:
|
|
|
114
131
|
console.info(status_tree)
|
|
115
132
|
|
|
116
133
|
|
|
117
|
-
def get_status_for_service(
|
|
134
|
+
def get_status_for_service(
|
|
135
|
+
service: Service, process_statuses: dict[str, ProcessInfo]
|
|
136
|
+
) -> str:
|
|
118
137
|
state = State()
|
|
119
138
|
|
|
120
139
|
modes = service.config.modes
|
|
@@ -141,7 +160,10 @@ def get_status_for_service(service: Service) -> str:
|
|
|
141
160
|
|
|
142
161
|
docker_compose_service_to_status = parse_docker_compose_status(status_json_results)
|
|
143
162
|
status_tree = generate_service_status_tree(
|
|
144
|
-
service.name,
|
|
163
|
+
service.name,
|
|
164
|
+
process_statuses,
|
|
165
|
+
dependency_graph,
|
|
166
|
+
docker_compose_service_to_status,
|
|
145
167
|
)
|
|
146
168
|
return status_tree
|
|
147
169
|
|
|
@@ -188,6 +210,7 @@ def get_status_json_results(
|
|
|
188
210
|
|
|
189
211
|
def generate_service_status_tree(
|
|
190
212
|
service_name: str,
|
|
213
|
+
process_statuses: dict[str, ProcessInfo],
|
|
191
214
|
dependency_graph: DependencyGraph,
|
|
192
215
|
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
193
216
|
indentation: str = "",
|
|
@@ -227,6 +250,7 @@ def generate_service_status_tree(
|
|
|
227
250
|
output.append(
|
|
228
251
|
process_service_with_containerized_runtime(
|
|
229
252
|
dependency,
|
|
253
|
+
process_statuses,
|
|
230
254
|
docker_compose_service_to_status,
|
|
231
255
|
indentation + BASE_INDENTATION,
|
|
232
256
|
dependency_graph,
|
|
@@ -261,6 +285,7 @@ def process_service_with_local_runtime(
|
|
|
261
285
|
|
|
262
286
|
def process_service_with_containerized_runtime(
|
|
263
287
|
dependency: DependencyNode,
|
|
288
|
+
process_statuses: dict[str, ProcessInfo],
|
|
264
289
|
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
265
290
|
indentation: str,
|
|
266
291
|
dependency_graph: DependencyGraph,
|
|
@@ -268,13 +293,14 @@ def process_service_with_containerized_runtime(
|
|
|
268
293
|
if len(dependency_graph.graph[dependency]) > 0:
|
|
269
294
|
return generate_service_status_tree(
|
|
270
295
|
dependency.name,
|
|
296
|
+
process_statuses,
|
|
271
297
|
dependency_graph,
|
|
272
298
|
docker_compose_service_to_status,
|
|
273
299
|
indentation,
|
|
274
300
|
)
|
|
275
301
|
else:
|
|
276
302
|
return generate_service_status_details(
|
|
277
|
-
dependency, docker_compose_service_to_status, indentation
|
|
303
|
+
dependency, process_statuses, docker_compose_service_to_status, indentation
|
|
278
304
|
)
|
|
279
305
|
|
|
280
306
|
|
|
@@ -301,16 +327,23 @@ def parse_docker_compose_status(
|
|
|
301
327
|
|
|
302
328
|
def generate_service_status_details(
|
|
303
329
|
dependency: DependencyNode,
|
|
330
|
+
process_statuses: dict[str, ProcessInfo],
|
|
304
331
|
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
305
332
|
indentation: str,
|
|
306
333
|
) -> str:
|
|
307
334
|
output = [f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:"]
|
|
308
335
|
|
|
336
|
+
# Handle supervisor dependencies
|
|
337
|
+
if dependency.dependency_type == DependencyType.SUPERVISOR:
|
|
338
|
+
return generate_supervisor_status_details(
|
|
339
|
+
dependency, process_statuses, indentation
|
|
340
|
+
)
|
|
341
|
+
|
|
309
342
|
if dependency.name not in docker_compose_service_to_status:
|
|
310
343
|
return "\n".join(
|
|
311
344
|
[
|
|
312
345
|
*output,
|
|
313
|
-
f"{indentation}{BASE_INDENTATION}Type: container",
|
|
346
|
+
(f"{indentation}{BASE_INDENTATION}Type: container"),
|
|
314
347
|
f"{indentation}{BASE_INDENTATION}Status: N/A",
|
|
315
348
|
]
|
|
316
349
|
)
|
|
@@ -349,7 +382,7 @@ def handle_started_service(dependency: DependencyNode, indentation: str) -> str:
|
|
|
349
382
|
f"{indentation}{BASE_INDENTATION}Runtime: local",
|
|
350
383
|
]
|
|
351
384
|
)
|
|
352
|
-
service_output = get_status_for_service(service_with_local_runtime)
|
|
385
|
+
service_output = get_status_for_service(service_with_local_runtime, {})
|
|
353
386
|
return "\n".join(
|
|
354
387
|
[f"{indentation}{line}" for line in service_output.splitlines()],
|
|
355
388
|
)
|
|
@@ -365,3 +398,56 @@ def format_health(health: str) -> str:
|
|
|
365
398
|
else Color.YELLOW
|
|
366
399
|
)
|
|
367
400
|
return f"{color}{health}{Color.RESET}"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def generate_supervisor_status_details(
|
|
404
|
+
dependency: DependencyNode,
|
|
405
|
+
process_statuses: dict[str, ProcessInfo],
|
|
406
|
+
indentation: str,
|
|
407
|
+
) -> str:
|
|
408
|
+
"""Generate status details for supervisor dependencies."""
|
|
409
|
+
output = [f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:"]
|
|
410
|
+
|
|
411
|
+
process_info = process_statuses.get(dependency.name)
|
|
412
|
+
|
|
413
|
+
if process_info is None:
|
|
414
|
+
return "\n".join(
|
|
415
|
+
[
|
|
416
|
+
*output,
|
|
417
|
+
f"{indentation}{BASE_INDENTATION}Type: process",
|
|
418
|
+
f"{indentation}{BASE_INDENTATION}Status: N/A (process not found)",
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
uptime_str = format_uptime(process_info["uptime"])
|
|
423
|
+
|
|
424
|
+
details = [
|
|
425
|
+
"Type: process",
|
|
426
|
+
f"Status: {process_info['state_name'].lower()}",
|
|
427
|
+
f"PID: {process_info['pid'] if process_info['pid'] > 0 else 'N/A'}",
|
|
428
|
+
f"Uptime: {uptime_str}",
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
output.extend(f"{indentation}{BASE_INDENTATION}{detail}" for detail in details)
|
|
432
|
+
|
|
433
|
+
return "\n".join(output)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def format_uptime(uptime_seconds: int) -> str:
|
|
437
|
+
"""Format uptime seconds into a human-readable string."""
|
|
438
|
+
SECONDS_PER_MINUTE = 60
|
|
439
|
+
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
|
|
440
|
+
|
|
441
|
+
td = timedelta(seconds=uptime_seconds)
|
|
442
|
+
days = td.days
|
|
443
|
+
hours, remainder = divmod(td.seconds, SECONDS_PER_HOUR)
|
|
444
|
+
minutes, seconds = divmod(remainder, SECONDS_PER_MINUTE)
|
|
445
|
+
|
|
446
|
+
if days > 0:
|
|
447
|
+
return f"{days}d {hours}h {minutes}m {seconds}s"
|
|
448
|
+
elif hours > 0:
|
|
449
|
+
return f"{hours}h {minutes}m {seconds}s"
|
|
450
|
+
elif minutes > 0:
|
|
451
|
+
return f"{minutes}m {seconds}s"
|
|
452
|
+
else:
|
|
453
|
+
return f"{seconds}s"
|