Skip to content

Commit ad86b66

Browse files
committed
feat: add support for FIFO (named pipe) files
Add support for reading .env files from FIFOs (named pipes) which may not have content immediately available when first accessed. This is useful for environments that expose .env data via FIFOs or mounted filesystems that may initially be empty. - Add _wait_for_file_content() function to handle both regular files and FIFOs with proper blocking/waiting logic - Update _get_stream() to use the new function for FIFO-aware file reading - Add select and time imports for FIFO handling
1 parent 85f4329 commit ad86b66

File tree

1 file changed

+49
-1
lines changed

1 file changed

+49
-1
lines changed

src/dotenv/main.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
import logging
33
import os
44
import pathlib
5+
import select
56
import shutil
67
import stat
78
import sys
89
import tempfile
10+
import time
911
from collections import OrderedDict
1012
from contextlib import contextmanager
1113
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
@@ -63,7 +65,11 @@ def __init__(
6365
@contextmanager
6466
def _get_stream(self) -> Iterator[IO[str]]:
6567
if self.dotenv_path and _is_file_or_fifo(self.dotenv_path):
66-
with open(self.dotenv_path, encoding=self.encoding) as stream:
68+
# Handle files that may need to wait for content to become available
69+
# This includes FIFOs (named pipes) and mounted filesystems that may
70+
# initially be empty or not yet populated when first accessed
71+
stream = _wait_for_file_content(self.dotenv_path, encoding=self.encoding)
72+
with stream:
6773
yield stream
6874
elif self.stream is not None:
6975
yield self.stream
@@ -420,6 +426,48 @@ def dotenv_values(
420426
).dict()
421427

422428

429+
def _wait_for_file_content(
430+
path: StrPath,
431+
encoding: Optional[str] = None,
432+
max_wait_time: float = 5.0,
433+
) -> IO[str]:
434+
"""
435+
Wait for file content to be available, handling both regular files and pipes (FIFOs).
436+
437+
Some environments expose .env data via FIFOs; reading the pipe produces content on demand.
438+
For FIFOs we block until the pipe is readable, then read once and return a StringIO over
439+
the decoded bytes. For regular files we open and return the handle directly.
440+
"""
441+
start_time = time.time()
442+
443+
try:
444+
st = os.stat(path)
445+
is_fifo = stat.S_ISFIFO(st.st_mode)
446+
except (FileNotFoundError, OSError):
447+
is_fifo = False
448+
449+
if not is_fifo:
450+
# Regular file path: open once and return immediately
451+
return open(path, encoding=encoding)
452+
453+
# FIFO path: block until readable (up to max_wait_time), then read once.
454+
# Open unbuffered binary so select() reflects readiness accurately before decoding.
455+
with open(path, "rb", buffering=0) as fifo:
456+
fd = fifo.fileno()
457+
timeout = max_wait_time - (time.time() - start_time)
458+
if timeout < 0:
459+
timeout = 0
460+
ready, _, _ = select.select([fd], [], [], timeout)
461+
if not ready:
462+
# If it never became readable, return empty content for caller to handle
463+
return io.StringIO("")
464+
465+
raw = fifo.read()
466+
text = raw.decode(encoding or "utf-8", errors="replace")
467+
# Return a fresh StringIO so caller can read from the start
468+
return io.StringIO(text)
469+
470+
423471
def _is_file_or_fifo(path: StrPath) -> bool:
424472
"""
425473
Return True if `path` exists and is either a regular file or a FIFO.

0 commit comments

Comments
 (0)