Add Home Assistant Stelloauth app #1
@@ -211,6 +211,7 @@ class ProcessManager:
|
|||||||
self._sleep = sleep
|
self._sleep = sleep
|
||||||
self._profile_path = profile_path
|
self._profile_path = profile_path
|
||||||
self._children: list[tuple[str, subprocess.Popen]] = []
|
self._children: list[tuple[str, subprocess.Popen]] = []
|
||||||
|
self._reaped_children: set[int] = set()
|
||||||
self._groups: list[int] = []
|
self._groups: list[int] = []
|
||||||
self._dead_groups: set[int] = set()
|
self._dead_groups: set[int] = set()
|
||||||
self._stopping = False
|
self._stopping = False
|
||||||
@@ -255,7 +256,19 @@ class ProcessManager:
|
|||||||
def _startup_stopping(self) -> bool:
|
def _startup_stopping(self) -> bool:
|
||||||
return self._stopping or self._first_exited_child() is not None
|
return self._stopping or self._first_exited_child() is not None
|
||||||
|
|
||||||
|
def _reap_exited_children(self) -> None:
|
||||||
|
for _name, process in self._children:
|
||||||
|
if process.pid in self._reaped_children or process.poll() is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
process.wait(timeout=None)
|
||||||
|
except OSError:
|
||||||
|
logging.error("Child process reap failed")
|
||||||
|
else:
|
||||||
|
self._reaped_children.add(process.pid)
|
||||||
|
|
||||||
def _live_groups(self) -> list[int]:
|
def _live_groups(self) -> list[int]:
|
||||||
|
self._reap_exited_children()
|
||||||
live_groups = []
|
live_groups = []
|
||||||
for pgid in self._groups:
|
for pgid in self._groups:
|
||||||
if pgid in self._dead_groups:
|
if pgid in self._dead_groups:
|
||||||
@@ -291,10 +304,14 @@ class ProcessManager:
|
|||||||
|
|
||||||
def _reap_all(self) -> None:
|
def _reap_all(self) -> None:
|
||||||
for _name, process in self._children:
|
for _name, process in self._children:
|
||||||
|
if process.pid in self._reaped_children:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
process.wait(timeout=None)
|
process.wait(timeout=None)
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error("Child process reap failed")
|
logging.error("Child process reap failed")
|
||||||
|
else:
|
||||||
|
self._reaped_children.add(process.pid)
|
||||||
|
|
||||||
def _shutdown(self) -> None:
|
def _shutdown(self) -> None:
|
||||||
if not self._children:
|
if not self._children:
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ class FakeProcess:
|
|||||||
self.returncode: int | None = None
|
self.returncode: int | None = None
|
||||||
self.ignores_term = ignores_term
|
self.ignores_term = ignores_term
|
||||||
self.wait_calls: list[float | None] = []
|
self.wait_calls: list[float | None] = []
|
||||||
|
self.on_wait = None
|
||||||
|
|
||||||
def poll(self) -> int | None:
|
def poll(self) -> int | None:
|
||||||
return self.returncode
|
return self.returncode
|
||||||
@@ -390,6 +391,8 @@ class FakeProcess:
|
|||||||
self.wait_calls.append(timeout)
|
self.wait_calls.append(timeout)
|
||||||
if self.returncode is None:
|
if self.returncode is None:
|
||||||
self.returncode = -9
|
self.returncode = -9
|
||||||
|
if self.on_wait is not None:
|
||||||
|
self.on_wait()
|
||||||
return self.returncode
|
return self.returncode
|
||||||
|
|
||||||
|
|
||||||
@@ -400,6 +403,7 @@ def manager_harness(
|
|||||||
cloak_probe=None,
|
cloak_probe=None,
|
||||||
stelloauth_probe=None,
|
stelloauth_probe=None,
|
||||||
ignores_term: tuple[bool, bool] = (False, False),
|
ignores_term: tuple[bool, bool] = (False, False),
|
||||||
|
live_descendants: tuple[bool, bool] = (False, False),
|
||||||
):
|
):
|
||||||
clock = FakeClock()
|
clock = FakeClock()
|
||||||
events: list[object] = []
|
events: list[object] = []
|
||||||
@@ -408,6 +412,17 @@ def manager_harness(
|
|||||||
FakeProcess(1002, ignores_term=ignores_term[1]),
|
FakeProcess(1002, ignores_term=ignores_term[1]),
|
||||||
]
|
]
|
||||||
groups = {process.pid: True for process in processes}
|
groups = {process.pid: True for process in processes}
|
||||||
|
leaders_reaped = {process.pid: False for process in processes}
|
||||||
|
term_delivered = {process.pid: False for process in processes}
|
||||||
|
for index, process in enumerate(processes):
|
||||||
|
def on_wait(index=index, process=process) -> None:
|
||||||
|
leaders_reaped[process.pid] = True
|
||||||
|
if not live_descendants[index] or (
|
||||||
|
term_delivered[process.pid] and not process.ignores_term
|
||||||
|
):
|
||||||
|
groups[process.pid] = False
|
||||||
|
|
||||||
|
process.on_wait = on_wait
|
||||||
group_checks: list[int] = []
|
group_checks: list[int] = []
|
||||||
spawned: list[FakeProcess] = []
|
spawned: list[FakeProcess] = []
|
||||||
|
|
||||||
@@ -420,10 +435,17 @@ def manager_harness(
|
|||||||
def killpg(pid: int, sent_signal: int) -> None:
|
def killpg(pid: int, sent_signal: int) -> None:
|
||||||
events.append(("signal", pid, sent_signal))
|
events.append(("signal", pid, sent_signal))
|
||||||
process = next(item for item in processes if item.pid == pid)
|
process = next(item for item in processes if item.pid == pid)
|
||||||
if sent_signal == signal.SIGKILL or not process.ignores_term:
|
if sent_signal == signal.SIGKILL:
|
||||||
groups[pid] = False
|
groups[pid] = False
|
||||||
if process.returncode is None:
|
if process.returncode is None:
|
||||||
process.returncode = -sent_signal
|
process.returncode = -sent_signal
|
||||||
|
else:
|
||||||
|
term_delivered[pid] = True
|
||||||
|
if not process.ignores_term:
|
||||||
|
if process.returncode is None:
|
||||||
|
process.returncode = -sent_signal
|
||||||
|
if leaders_reaped[pid]:
|
||||||
|
groups[pid] = False
|
||||||
|
|
||||||
def group_alive(pid: int) -> bool:
|
def group_alive(pid: int) -> bool:
|
||||||
group_checks.append(pid)
|
group_checks.append(pid)
|
||||||
@@ -583,7 +605,9 @@ def test_child_exit_stops_sibling_and_returns_failure(
|
|||||||
def test_exited_leader_with_live_descendants_still_gets_group_sigterm(
|
def test_exited_leader_with_live_descendants_still_gets_group_sigterm(
|
||||||
supervisor, tmp_path: Path, monkeypatch
|
supervisor, tmp_path: Path, monkeypatch
|
||||||
) -> None:
|
) -> None:
|
||||||
harness = manager_harness(supervisor, tmp_path)
|
harness = manager_harness(
|
||||||
|
supervisor, tmp_path, live_descendants=(True, False)
|
||||||
|
)
|
||||||
triggered = False
|
triggered = False
|
||||||
|
|
||||||
def sleep(delay: float) -> None:
|
def sleep(delay: float) -> None:
|
||||||
@@ -603,6 +627,56 @@ def test_exited_leader_with_live_descendants_still_gets_group_sigterm(
|
|||||||
assert [process.wait_calls for process in harness.processes] == [[None], [None]]
|
assert [process.wait_calls for process in harness.processes] == [[None], [None]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_leader_exit_on_sigterm_without_descendants_finishes_without_waiting(
|
||||||
|
supervisor, tmp_path: Path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
harness = manager_harness(supervisor, tmp_path)
|
||||||
|
|
||||||
|
def stelloauth_probe() -> None:
|
||||||
|
harness.manager._handle_signal(signal.SIGTERM, None)
|
||||||
|
|
||||||
|
harness.manager._stelloauth_probe = stelloauth_probe
|
||||||
|
monkeypatch.setattr(supervisor.signal, "signal", lambda *_args: None)
|
||||||
|
|
||||||
|
assert harness.manager.run() == 0
|
||||||
|
assert harness.clock.now == 0.0
|
||||||
|
assert not any(
|
||||||
|
event[0] == "signal" and event[2] == signal.SIGKILL
|
||||||
|
for event in harness.events
|
||||||
|
)
|
||||||
|
assert [process.wait_calls for process in harness.processes] == [[None], [None]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_exited_leader_with_live_descendants_still_uses_deadline_and_sigkill(
|
||||||
|
supervisor, tmp_path: Path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
harness = manager_harness(
|
||||||
|
supervisor,
|
||||||
|
tmp_path,
|
||||||
|
ignores_term=(True, False),
|
||||||
|
live_descendants=(True, False),
|
||||||
|
)
|
||||||
|
triggered = False
|
||||||
|
|
||||||
|
def sleep(delay: float) -> None:
|
||||||
|
nonlocal triggered
|
||||||
|
harness.clock.sleep(delay)
|
||||||
|
if not triggered:
|
||||||
|
triggered = True
|
||||||
|
harness.processes[0].returncode = 7
|
||||||
|
|
||||||
|
harness.manager._sleep = sleep
|
||||||
|
monkeypatch.setattr(supervisor.signal, "signal", lambda *_args: None)
|
||||||
|
|
||||||
|
assert harness.manager.run() == 7
|
||||||
|
assert harness.clock.now == pytest.approx(10.25)
|
||||||
|
assert ("signal", 1001, signal.SIGTERM) in harness.events
|
||||||
|
assert ("signal", 1001, signal.SIGKILL) in harness.events
|
||||||
|
assert ("signal", 1002, signal.SIGTERM) in harness.events
|
||||||
|
assert ("signal", 1002, signal.SIGKILL) not in harness.events
|
||||||
|
assert [process.wait_calls for process in harness.processes] == [[None], [None]]
|
||||||
|
|
||||||
|
|
||||||
def test_group_disappearance_ends_shared_wait_without_sigkill(
|
def test_group_disappearance_ends_shared_wait_without_sigkill(
|
||||||
supervisor, tmp_path: Path, monkeypatch
|
supervisor, tmp_path: Path, monkeypatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user