Skip to content

Commit 4543837

Browse files
authored
Enhance dotenv run: Switch to execvpe for better resource management and signal handling (#523)
The current implementation of `dotenv run` CLI uses `subprocess.Popen`, which spawns a child process to execute the specified command. ``` p = Popen(command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env) ``` After spawning the child process, it exits with the same exit code returned by the child process. ``` ret = run_command(commandline, dotenv_as_dict) exit(ret) ``` ### We can enhance `dotenv run` usage dramatically while preserving exactly the same behaviour By switching to `os.execvpe` instead of `subprocess.Popen`, we can replace the parent dotenv process with the new process specified by the user. This results in only one active process—the program the user intended to run. **Benefits:** 1. No hanging parent process `dotenv run` acts as a launcher, so after executing `dotenv run redis-server`, only the Redis server process remains. The dotenv process, along with its Python interpreter, is completely replaced. This prevents the dotenv process from consuming RAM and other resources, which would otherwise persist until the Redis server exits. 2. Proper signal handling When using `subprocess.Popen`, the parent process (e.g., `dotenv`) remains responsible for handling and forwarding signals, which can lead to issues if the command doesn’t receive them directly. For instance, in Docker, if Redis was started without `exec`, it may not get important signals like `SIGTERM` when the container stops, potentially resulting in improper shutdowns or zombie processes. Using `os.execvpe` ensures that the command receives signals directly, improving reliability and making `dotenv` more suitable for production environments and improving reliability for DevOps engineers managing containerized applications. All current logic will be preserved because dotenv run does not do anything special except propagate the child process exit code. Thanks / @eekstunt
1 parent 08937a1 commit 4543837

File tree

1 file changed

+7
-16
lines changed

1 file changed

+7
-16
lines changed

src/dotenv/cli.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import shlex
44
import sys
55
from contextlib import contextmanager
6-
from subprocess import Popen
76
from typing import Any, Dict, IO, Iterator, List
87

98
try:
@@ -161,14 +160,13 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
161160
if not commandline:
162161
click.echo('No command given.')
163162
exit(1)
164-
ret = run_command(commandline, dotenv_as_dict)
165-
exit(ret)
163+
run_command(commandline, dotenv_as_dict)
166164

167165

168-
def run_command(command: List[str], env: Dict[str, str]) -> int:
169-
"""Run command in sub process.
166+
def run_command(command: List[str], env: Dict[str, str]) -> None:
167+
"""Replace the current process with the specified command.
170168
171-
Runs the command in a sub process with the variables from `env`
169+
Replaces the current process with the specified command and the variables from `env`
172170
added in the current environment variables.
173171
174172
Parameters
@@ -180,20 +178,13 @@ def run_command(command: List[str], env: Dict[str, str]) -> int:
180178
181179
Returns
182180
-------
183-
int
184-
The return code of the command
181+
None
182+
This function does not return any value. It replaces the current process with the new one.
185183
186184
"""
187185
# copy the current environment variables and add the vales from
188186
# `env`
189187
cmd_env = os.environ.copy()
190188
cmd_env.update(env)
191189

192-
p = Popen(command,
193-
universal_newlines=True,
194-
bufsize=0,
195-
shell=False,
196-
env=cmd_env)
197-
_, _ = p.communicate()
198-
199-
return p.returncode
190+
os.execvpe(command[0], args=command, env=cmd_env)

0 commit comments

Comments
 (0)