• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.21
/src/python/pants/pantsd/pants_daemon_core.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
3✔
5

6
import logging
3✔
7
import threading
3✔
8
from collections.abc import Iterator
3✔
9
from contextlib import contextmanager
3✔
10
from typing import Any, Protocol
3✔
11

12
from pants.build_graph.build_configuration import BuildConfiguration
3✔
13
from pants.engine.env_vars import CompleteEnvironmentVars
3✔
14
from pants.engine.internals.native_engine import PyExecutor
3✔
15
from pants.engine.unions import UnionMembership
3✔
16
from pants.init.engine_initializer import EngineInitializer, GraphScheduler
3✔
17
from pants.init.options_initializer import OptionsInitializer
3✔
18
from pants.option.bootstrap_options import AuthPluginResult, DynamicRemoteOptions
3✔
19
from pants.option.option_value_container import OptionValueContainer
3✔
20
from pants.option.options_bootstrapper import OptionsBootstrapper
3✔
21
from pants.option.options_diff import summarize_dynamic_options_diff, summarize_options_map_diff
3✔
22
from pants.option.options_fingerprinter import OptionsFingerprinter
3✔
23
from pants.option.scope import GLOBAL_SCOPE
3✔
24
from pants.pantsd.service.pants_service import PantsServices
3✔
25

26
logger = logging.getLogger(__name__)
3✔
27

28

29
class PantsServicesConstructor(Protocol):
3✔
30
    def __call__(
31
        self,
32
        bootstrap_options: OptionValueContainer,
33
        graph_scheduler: GraphScheduler,
34
    ) -> PantsServices: ...
35

36

37
class PantsDaemonCore:
3✔
38
    """A container for the state of a PantsDaemon that is affected by the bootstrap options.
39

40
    This class also serves to avoid a reference cycle between DaemonPantsRunner and PantsDaemon,
41
    which both have a reference to the core, and use it to get access to the Scheduler and current
42
    PantsServices.
43
    """
44

45
    def __init__(
3✔
46
        self,
47
        options_bootstrapper: OptionsBootstrapper,
48
        executor: PyExecutor,
49
        services_constructor: PantsServicesConstructor,
50
    ):
51
        self._options_initializer = OptionsInitializer(options_bootstrapper, executor)
×
52
        self._executor = executor
×
53
        self._services_constructor = services_constructor
×
54
        self._lifecycle_lock = threading.RLock()
×
55
        # N.B. This Event is used as nothing more than an atomic flag - nothing waits on it.
56
        self._kill_switch = threading.Event()
×
57

58
        self._scheduler: GraphScheduler | None = None
×
59
        self._services: PantsServices | None = None
×
60

61
        self._prior_options_map: dict[str, Any] | None = None
×
62
        self._prior_dynamic_remote_options: DynamicRemoteOptions | None = None
×
63
        self._prior_auth_plugin_result: AuthPluginResult | None = None
×
64

65
    def is_valid(self) -> bool:
3✔
66
        """Return true if the core is valid.
67

68
        This mostly means confirming that if any services have been started, that they are still
69
        alive.
70
        """
71
        if self._kill_switch.is_set():
×
72
            logger.error("Client failed to create a Scheduler: shutting down.")
×
73
            return False
×
74
        with self._lifecycle_lock:
×
75
            if self._services is None:
×
76
                return True
×
77
            return self._services.are_all_alive()
×
78

79
    @contextmanager
3✔
80
    def _handle_exceptions(self) -> Iterator[None]:
3✔
81
        try:
×
82
            yield
×
83
        except Exception as e:
×
84
            self._kill_switch.set()
×
85
            self._scheduler = None
×
86
            raise e
×
87

88
    def _initialize(
3✔
89
        self,
90
        bootstrap_options: OptionValueContainer,
91
        build_config: BuildConfiguration,
92
        dynamic_remote_options: DynamicRemoteOptions,
93
        scheduler_restart_explanation: str | None,
94
    ) -> None:
95
        """(Re-)Initialize the scheduler.
96

97
        Must be called under the lifecycle lock.
98
        """
99
        try:
×
100
            logger.info(
×
101
                f"{scheduler_restart_explanation}. Reinitializing scheduler..."
102
                if scheduler_restart_explanation
103
                else "Initializing scheduler..."
104
            )
105
            if self._services:
×
106
                self._services.shutdown()
×
107
            self._scheduler = EngineInitializer.setup_graph(
×
108
                bootstrap_options, build_config, dynamic_remote_options, self._executor
109
            )
110

111
            self._services = self._services_constructor(bootstrap_options, self._scheduler)
×
112
            logger.info("Scheduler initialized.")
×
113
        except Exception as e:
×
114
            self._kill_switch.set()
×
115
            self._scheduler = None
×
116
            raise e
×
117

118
    def prepare(
3✔
119
        self, options_bootstrapper: OptionsBootstrapper, env: CompleteEnvironmentVars
120
    ) -> tuple[GraphScheduler, OptionsInitializer]:
121
        """Get a scheduler for the given options_bootstrapper.
122

123
        Runs in a client context (generally in DaemonPantsRunner) so logging is sent to the client.
124
        """
125

126
        with self._handle_exceptions():
×
127
            build_config = self._options_initializer.build_config(options_bootstrapper, env)
×
128
            union_membership = UnionMembership.from_rules(build_config.union_rules)
×
129
            options = self._options_initializer.options(
×
130
                options_bootstrapper, env, build_config, union_membership, raise_=True
131
            )
132

133
        scheduler_restart_explanation: str | None = None
×
134

135
        # Because these options are computed dynamically via side effects like reading from a file,
136
        # they need to be re-evaluated every run. We only reinitialize the scheduler if changes
137
        # were made, though.
138
        dynamic_remote_options, auth_plugin_result = DynamicRemoteOptions.from_options(
×
139
            options,
140
            env,
141
            self._prior_auth_plugin_result,
142
            remote_auth_plugin_func=build_config.remote_auth_plugin_func,
143
        )
144
        remote_options_changed = dynamic_remote_options != self._prior_dynamic_remote_options
×
145
        if self._prior_dynamic_remote_options is not None and remote_options_changed:
×
146
            diff = summarize_dynamic_options_diff(
×
147
                self._prior_dynamic_remote_options, dynamic_remote_options
148
            )
149
            scheduler_restart_explanation = f"Remote cache/execution options updated: {diff}"
×
150

151
        options_map = OptionsFingerprinter.options_map_for_scope(
×
152
            GLOBAL_SCOPE,
153
            options_bootstrapper.bootstrap_options,
154
        )
155
        bootstrap_options_changed = options_map != self._prior_options_map
×
156
        if self._prior_options_map is not None and bootstrap_options_changed:
×
157
            diff = summarize_options_map_diff(self._prior_options_map, options_map)
×
158
            scheduler_restart_explanation = f"Initialization options changed: {diff}"
×
159

160
        with self._lifecycle_lock:
×
161
            if self._scheduler is None or scheduler_restart_explanation:
×
162
                # No existing options to compare (first run) or options have changed. Create a new
163
                # scheduler and services.
164
                bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
×
165
                assert bootstrap_options is not None
×
166
                with self._handle_exceptions():
×
167
                    self._initialize(
×
168
                        bootstrap_options,
169
                        build_config,
170
                        dynamic_remote_options,
171
                        scheduler_restart_explanation,
172
                    )
173

174
            self._prior_options_map = options_map
×
175
            self._prior_dynamic_remote_options = dynamic_remote_options
×
176
            self._prior_auth_plugin_result = auth_plugin_result
×
177

178
            assert self._scheduler is not None
×
179
            return self._scheduler, self._options_initializer
×
180

181
    def shutdown(self) -> None:
3✔
182
        with self._lifecycle_lock:
×
183
            if self._services is not None:
×
184
                self._services.shutdown()
×
185
                self._services = None
×
186
            if self._scheduler is not None:
×
187
                self._scheduler.scheduler.shutdown()
×
188
                self._scheduler = None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc