blob: b0c2cacddddd901252cd147c0207a1736142b244 [file] [log] [blame]
# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Fetch active Project Builder Context."""
import concurrent.futures
from contextvars import ContextVar
from dataclasses import dataclass, field
from enum import Enum
import logging
import subprocess
from typing import Dict, Optional
from pw_build.build_recipe import BuildRecipe
_LOG = logging.getLogger('pw_build.watch')
def _wait_for_terminate_then_kill(
proc: subprocess.Popen, timeout: int = 3
) -> int:
"""Wait for a process to end, then kill it if the timeout expires."""
returncode = 1
try:
returncode = proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
_LOG.debug('Killing %s', proc)
proc.kill()
return returncode
class ProjectBuilderState(Enum):
IDLE = 'IDLE'
BUILDING = 'BUILDING'
ABORT = 'ABORT'
@dataclass
class ProjectBuilderContext:
"""Maintains the state of running builds and active subproccesses."""
current_state: Optional[ProjectBuilderState] = ProjectBuilderState.IDLE
desired_state: Optional[ProjectBuilderState] = ProjectBuilderState.BUILDING
procs: Dict[BuildRecipe, Optional[subprocess.Popen]] = field(
default_factory=dict
)
def register_process(
self, recipe: BuildRecipe, proc: subprocess.Popen
) -> None:
self.procs[recipe] = proc
def terminate_and_wait(self) -> None:
"""End a subproces either cleanly or with a kill signal."""
if self.is_idle() or self.should_abort():
return
self._signal_abort()
with concurrent.futures.ThreadPoolExecutor(
max_workers=len(self.procs)
) as executor:
futures = []
for _recipe, proc in self.procs.items():
if proc is None:
continue
_LOG.debug('Wait for %s', proc)
futures.append(
executor.submit(_wait_for_terminate_then_kill, proc)
)
for future in concurrent.futures.as_completed(futures):
future.result()
_LOG.debug('Wait for terminate DONE')
self.set_idle()
def _signal_abort(self) -> None:
self.desired_state = ProjectBuilderState.ABORT
def should_abort(self) -> bool:
return self.desired_state == ProjectBuilderState.ABORT
def is_building(self) -> bool:
return self.current_state == ProjectBuilderState.BUILDING
def is_idle(self) -> bool:
return self.current_state == ProjectBuilderState.IDLE
def set_idle(self) -> None:
self.current_state = ProjectBuilderState.IDLE
self.desired_state = ProjectBuilderState.IDLE
def set_building(self) -> None:
self.current_state = ProjectBuilderState.BUILDING
self.desired_state = ProjectBuilderState.BUILDING
PROJECT_BUILDER_CONTEXTVAR = ContextVar(
'pw_build_project_builder_state', default=ProjectBuilderContext()
)
def get_project_builder_context():
return PROJECT_BUILDER_CONTEXTVAR.get()