Skip to content

Commit 850b958

Browse files
committed
Avoid subprocess memory copy when c library supports posix_spawn
By default python 3.10 will use the fork() which has to copy memory of the parent process (in our case this can be huge since Home Assistant core can use hundreds of megabytes of RAM). By using posix_spawn this is avoided and subprocess creation does not get discernibly slow the larger the Home Assistant python process grows. In python 3.11 vfork will also be available python/cpython#80004 (comment) python/cpython#11671 but we won't always be able to use it and posix_spawn is considered safer https://bugzilla.kernel.org/show_bug.cgi?id=215813#c14 The subprocess library doesn't know about musl though even though it supports posix_spawn https://git.musl-libc.org/cgit/musl/log/src/process/posix_spawn.c so we have to teach it since it only has checks for glibc https://github.com/python/cpython/blob/1b736838e6ae1b4ef42cdd27c2708face908f92c/Lib/subprocess.py#L745 The constant is documented as being able to be flipped here: https://docs.python.org/3/library/subprocess.html#disabling-use-of-vfork-or-posix-spawn
1 parent 00c356e commit 850b958

File tree

9 files changed

+39
-5
lines changed

9 files changed

+39
-5
lines changed

homeassistant/auth/providers/command_line.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async def async_validate_login(self, username: str, password: str) -> None:
6868
*self.config[CONF_ARGS],
6969
env=env,
7070
stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None,
71+
close_fds=False, # required for posix_spawn
7172
)
7273
stdout, _ = await process.communicate()
7374
except OSError as err:

homeassistant/components/command_line/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ def call_shell_with_timeout(
1818
try:
1919
_LOGGER.debug("Running command: %s", command)
2020
subprocess.check_output(
21-
command, shell=True, timeout=timeout # nosec # shell by design
21+
command,
22+
shell=True, # nosec # shell by design
23+
timeout=timeout,
24+
close_fds=False, # required for posix_spawn
2225
)
2326
return 0
2427
except subprocess.CalledProcessError as proc_exception:
@@ -41,7 +44,10 @@ def check_output_or_log(command: str, timeout: int) -> str | None:
4144
"""Run a shell command with a timeout and return the output."""
4245
try:
4346
return_value = subprocess.check_output(
44-
command, shell=True, timeout=timeout # nosec # shell by design
47+
command,
48+
shell=True, # nosec # shell by design
49+
timeout=timeout,
50+
close_fds=False, # required for posix_spawn
4551
)
4652
return return_value.strip().decode("utf-8")
4753
except subprocess.CalledProcessError as err:

homeassistant/components/command_line/notify.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def send_message(self, message="", **kwargs) -> None:
5252
self.command,
5353
universal_newlines=True,
5454
stdin=subprocess.PIPE,
55+
close_fds=False, # required for posix_spawn
5556
shell=True, # nosec # shell by design
5657
) as proc:
5758
try:

homeassistant/components/ping/binary_sensor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ async def async_ping(self):
227227
stdin=None,
228228
stdout=asyncio.subprocess.PIPE,
229229
stderr=asyncio.subprocess.PIPE,
230+
close_fds=False, # required for posix_spawn
230231
)
231232
try:
232233
out_data, out_error = await asyncio.wait_for(

homeassistant/components/ping/device_tracker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ def __init__(self, ip_address, dev_id, hass, config, privileged):
5555
def ping(self):
5656
"""Send an ICMP echo request and return True if success."""
5757
with subprocess.Popen(
58-
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
58+
self._ping_cmd,
59+
stdout=subprocess.PIPE,
60+
stderr=subprocess.DEVNULL,
61+
close_fds=False, # required for posix_spawn
5962
) as pinger:
6063
try:
6164
pinger.communicate(timeout=1 + PING_TIMEOUT)

homeassistant/components/rpi_camera/camera.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
def kill_raspistill(*args):
3333
"""Kill any previously running raspistill process.."""
3434
with subprocess.Popen(
35-
["killall", "raspistill"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
35+
["killall", "raspistill"],
36+
stdout=subprocess.DEVNULL,
37+
stderr=subprocess.STDOUT,
38+
close_fds=False, # required for posix_spawn
3639
):
3740
pass
3841

@@ -132,7 +135,10 @@ def __init__(self, device_info):
132135
# Therefore it must not be wrapped with "with", since that
133136
# waits for the subprocess to exit before continuing.
134137
subprocess.Popen( # pylint: disable=consider-using-with
135-
cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
138+
cmd_args,
139+
stdout=subprocess.DEVNULL,
140+
stderr=subprocess.STDOUT,
141+
close_fds=False, # required for posix_spawn
136142
)
137143

138144
def camera_image(

homeassistant/components/shell_command/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async def async_service_handler(service: ServiceCall) -> None:
6565
stdin=None,
6666
stdout=asyncio.subprocess.PIPE,
6767
stderr=asyncio.subprocess.PIPE,
68+
close_fds=False, # required for posix_spawn
6869
)
6970
else:
7071
# Template used. Break into list and use create_subprocess_exec
@@ -76,6 +77,7 @@ async def async_service_handler(service: ServiceCall) -> None:
7677
stdin=None,
7778
stdout=asyncio.subprocess.PIPE,
7879
stderr=asyncio.subprocess.PIPE,
80+
close_fds=False, # required for posix_spawn
7981
)
8082

8183
process = await create_process

homeassistant/runner.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import asyncio
55
import dataclasses
66
import logging
7+
import os
8+
import subprocess
79
import threading
810
import traceback
911
from typing import Any
@@ -26,6 +28,7 @@
2628
#
2729
MAX_EXECUTOR_WORKERS = 64
2830
TASK_CANCELATION_TIMEOUT = 5
31+
ALPINE_RELEASE_FILE = "/etc/alpine-release"
2932

3033
_LOGGER = logging.getLogger(__name__)
3134

@@ -120,6 +123,16 @@ async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int:
120123

121124
def run(runtime_config: RuntimeConfig) -> int:
122125
"""Run Home Assistant."""
126+
if not subprocess._USE_POSIX_SPAWN: # pylint: disable=protected-access
127+
# The subprocess module does not know about Alpine Linux/musl
128+
# and will use fork() instead of posix_spawn() which significantly
129+
# less efficient. This is a workaround to force posix_spawn()
130+
# on Alpine Linux which is supported by musl.
131+
_exists = os.path.exists
132+
subprocess._USE_POSIX_SPAWN = _exists( # pylint: disable=protected-access
133+
ALPINE_RELEASE_FILE
134+
)
135+
123136
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
124137
# Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
125138
loop = asyncio.new_event_loop()

homeassistant/util/package.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ async def async_get_user_site(deps_dir: str) -> str:
121121
stdout=asyncio.subprocess.PIPE,
122122
stderr=asyncio.subprocess.DEVNULL,
123123
env=env,
124+
close_fds=False, # required for posix_spawn
124125
)
125126
stdout, _ = await process.communicate()
126127
lib_dir = stdout.decode().strip()

0 commit comments

Comments
 (0)