Skip to content

Conversation

@PaulSD
Copy link
Contributor

@PaulSD PaulSD commented Sep 21, 2025

#169 causes lots of problems, because many bashio functions produce output (on stdout) that is meant to be captured by the caller, and log messages produced by those functions are now intermixed with that output and captured by the caller instead of being sent to an independent stream for logging.

For example, configuring log_level: trace on (almost) any HA add-on now causes the add-on to fail because the log.trace message in bashio::addon.config() gets mixed with the returned config value (effectively corrupting the returned config value) and causes the jq command in bashio::config() to fail, effectively breaking config parsing entirely.

Some reports of user issues related to this are here: hassio-addons/addon-nut#451

I think this can be fixed by duplicating the original/parent stdout file handle to fd 3, then logging to fd 3 instead of stdout, as implemented in this PR. This works because each subshell/child gets its own fd 0/1/2 (overriding the parent's fd 0/1/2 but piping from/to the parent's fd 0/1/2 unless redirected) while inheriting all other fds from the parent (so in this case, the copy of the parent's stdout in fd 3 will be inherited by all subshells/children), effectively providing an independent log stream that can be used without affecting output capture for subshells/children.

However, as I don't know all of the ways bashio is used, it is possible that this doesn't solve all of the problems caused by #169.

Note that any add-on images published since #169 was merged (09/11/2025 15:50 UTC) will need to be re-published after bashio is fixed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved logging reliability so messages consistently appear even when commands capture output or run in subshells.
    • Prevented logs from interfering with command outputs, reducing noise in pipelines and script integrations.
    • Preserved log levels, colorized formatting, and stable emission targets so logs remain readable and consistent.

Copy link
Contributor

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#169 causes lots of problems, because many bashio functions produce output (on stdout) that is meant to be captured by the caller, and log messages produced by those functions are now intermixed with that output and captured by the caller instead of being sent to an independent stream.

Nice catch!

I think this can be fixed by duplicating the original/parent stdout file handle to fd 3, then logging to fd 3 instead of stdout, as implemented in this PR. This works because each subshell/child gets its own fd 0/1/2 (overriding the parent's fd 0/1/2 but piping from/to the parent's fd 0/1/2 unless redirected) while inheriting all other fds from the parent (so in this case, the copy of the parent's stdout in fd 3 will be inherited by all subshells/children), effectively providing an independent log stream that can be used without affecting output capture for subshells/children.

Hm, I see that works as long as the only thing we rely on is output from child processes. E.g. if we were using echo something in some bashio script and parse that in parent, it still would mix with the log output. But I'd consider that unexpected use.

I guess another alternative would be to revert #169, and left things as they were. But I think this approach is worth a try. I'd guess this has a high chance to work in pretty much all cases. One suggestion though to improve the implementation, just in case fd 3 is used somewhere (which I think s6 is actually, iirc).

@coderabbitai
Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

Redirects bashio logging in lib/log.sh to write through a preserved original-stdout file descriptor (LOG_FD) created with exec {LOG_FD}>&1. All color/level log functions now use printf ... >&"$LOG_FD". No public function signatures changed.

Changes

Cohort / File(s) Summary
Logging FD redirection
lib/log.sh
Add guard and exec {LOG_FD}>&1 to preserve original STDOUT; replace echo -e with printf '%b\n' ... >&"$LOG_FD" across bashio::log and all color/level variants; final bashio::log.log output redirected to LOG_FD; no exported/public declarations changed.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Script
    participant LogFn as bashio::log.* 
    participant Core as bashio::log.log
    participant FD as LOG_FD\n(dup of original STDOUT)
    participant Out as Terminal/Log Sink

    Note over Script,Out: Logging flow using preserved original STDOUT (LOG_FD)

    Script->>LogFn: call e.g. bashio::log.green("msg")
    LogFn->>Core: format & delegate
    Core-->>FD: printf '%b\n' "formatted" >&"$LOG_FD"
    FD-->>Out: emit log line

    Note over Script,FD: Subshell/stdout captures no longer intercept logging (logs go to LOG_FD)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

bugfix

Suggested reviewers

  • frenck

Poem

A nibble of logs in a warren of bash,
I burrow a tunnel for outputs to dash.
Saved stdout gleams, a secret FD,
Subshells slumber while my messages flee.
Thump-thump! — tidy lines hop out with glee.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Use parent STDOUT for logging output" is concise and accurately summarizes the main change—redirecting logging to the parent process's stdout (via a duplicated fd) to avoid contaminating subshell stdout—and aligns with the PR objectives and code changes described.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
lib/log.sh (2)

10-14: Solid approach preserving parent STDOUT; add an idempotency guard to avoid FD leaks on re-source

If this file is sourced multiple times, a new FD will be allocated each time. Guard the setup so it only runs if LOG_FD isn’t already a valid descriptor.

Apply this diff:

-# Preserve the original STDOUT on a free fd so that we can log to it without
-# interfering with the STDOUT of subshells and child processes whose output we
-# want to capture for other purposes.
-exec {LOG_FD}>&1
+# Preserve the original STDOUT on a free fd so that we can log to it without
+# interfering with the STDOUT of subshells and child processes whose output we
+# want to capture for other purposes.
+# Only allocate if not already a valid, open FD (prevents leaks on re-source).
+if ! : >&"$LOG_FD" 2>/dev/null; then
+    exec {LOG_FD}>&1
+fi

23-23: Use printf instead of echo -e to avoid escape/portability pitfalls

printf is safer and avoids oddities with echo -e and messages starting with “-”.

Apply this diff:

-    echo -e "${message}" >&"$LOG_FD"
+    printf '%s\n' "${message}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_RED}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_RED}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_GREEN}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_GREEN}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_YELLOW}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_YELLOW}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_BLUE}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_BLUE}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_MAGENTA}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_MAGENTA}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${__BASHIO_COLORS_CYAN}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
+    printf '%b\n' "${__BASHIO_COLORS_CYAN}${message}${__BASHIO_COLORS_RESET}" >&"$LOG_FD"
-    echo -e "${output}" >&"$LOG_FD"
+    printf '%b\n' "${output}" >&"$LOG_FD"

Also applies to: 35-35, 47-47, 59-59, 71-71, 83-83, 95-95, 123-123

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1ae1ce and 0c3b370.

📒 Files selected for processing (1)
  • lib/log.sh (9 hunks)
🔇 Additional comments (1)
lib/log.sh (1)

106-126: Core fix LGTM — log.log now writes to preserved STDOUT, preventing corruption

This redirects structured log lines to LOG_FD, addressing the regression where logs mixed with function stdout.

@PaulSD PaulSD marked this pull request as draft September 23, 2025 14:05
Copy link
Contributor

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@PaulSD PaulSD marked this pull request as ready for review September 23, 2025 17:21
@PaulSD
Copy link
Contributor Author

PaulSD commented Sep 23, 2025

Made a few updates for safety (defensive programming).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
lib/log.sh (1)

10-16: Harden LOG_FD validation and add a fallback if STDOUT is closed

Avoid honoring 0/1/2 for LOG_FD and provide a safe fallback to STDERR if duplicating STDOUT fails (e.g., stdout closed). This reduces the risk of writes going to an unreadable FD.

-# Unless $LOG_FD is already set to a valid fd
-if ! [[ "$LOG_FD" =~ ^[0-9]+$ ]] || ! { true >&"$LOG_FD" ; } 2>/dev/null ; then
-  # Preserve the original STDOUT on a free fd (stored in $LOG_FD) so that we can
-  # log to it without interfering with the STDOUT of subshells whose output we
-  # want to capture for other purposes.
-  exec {LOG_FD}>&1
-fi
+# Unless $LOG_FD is already set to a valid fd (>=3) that we can duplicate to
+if ! [[ "$LOG_FD" =~ ^[3-9][0-9]*$ ]] || ! { : >&"$LOG_FD"; } 2>/dev/null; then
+  # Preserve the original STDOUT on a free fd (stored in $LOG_FD) so that we can
+  # log to it without interfering with the STDOUT of subshells whose output we
+  # want to capture for other purposes. If STDOUT is not available, fall back to STDERR.
+  if ! exec {LOG_FD}>&1 2>/dev/null; then
+    exec {LOG_FD}>&2
+  fi
+fi
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3b370 and c8982c2.

📒 Files selected for processing (1)
  • lib/log.sh (9 hunks)
🔇 Additional comments (1)
lib/log.sh (1)

26-26: Good switch to printf to LOG_FD — approved

All printf calls in lib/log.sh (lines 26, 38, 50, 62, 74, 86, 98, 126) are redirected with >&"$LOG_FD"; no echo -e or explicit >&1/>&2 matches found.

Copy link
Member

@frenck frenck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @PaulSD 👍

../Frenck

@frenck frenck merged commit 8054419 into hassio-addons:main Sep 23, 2025
4 checks passed
@agners
Copy link
Contributor

agners commented Sep 23, 2025

I was actually a bit confused as why this worked even without the guard introduced in d998311. I've tested the NUT case without guard, and it worked. We do command substitution $(), so wouldn't the subshell then just log into the same output? 🤔

So for one, a subshell inherits the environment variable even without exporting. But subshells are still separate processes.

However, it seems that $() does not actually creates a subshell process if its a function (which it is in bashio::config case). So we are actually still in the same shell, simply calling a function. From what I understand $() then redirects the stdout of that function call (causing stdout to e new fd?), while the log calls from the same shell write to the $LOG_FD which is still the stdout of the bashio script itself.

But the d998311 certainly don't hurt, e.g. if we were to actually call another, separate bashio script using subshell syntax () or substitution $()

@PaulSD
Copy link
Contributor Author

PaulSD commented Sep 27, 2025

@agners

There are several unintuitive things that are relevant to the behavior here:

First, file handles/descriptors created in BASH are inherited by / exported to commands run by bash (unless overridden using bash redirects when running the command). For example:

$ cat test1
#!/bin/bash
exec 5>&1
./test2 > test.out 2> test.err
$ cat test2
#!/bin/bash
echo here >&5
ls /proc/$BASHPID/fd/
$ ./test1
here
$ cat test.out
0 -> /dev/pts/18
1 -> test.out
2 -> test.err
5 -> /dev/pts/18
255 -> test2
$

Note that test2 runs in a completely different process than test1, but the fd 5 created in test1 is available in test2 when it is run by test1.
Also note that fd 5 in test2 still points to the original fd 1 of test1 regardless of the output redirection applied when running test2.
So, basically, once we run exec 5>&1, fd 5 becomes available to all child processes, and we can capture stdout/stderr of any child process, without affecting the child's ability to write directly to the parent's stdout via fd 5.
This explains why commands run via $() still log to the correct place without explicitly setting up LOG_FD again in every subshell or child.
In the bashio case, we set up LOG_FD when lib/log.sh is sourced (at or near the beginning of a bashio script), then later calls to log functions in bashio can write to LOG_FD without worrying about any stdout/stderr redirections.

Next, the simple case works fine without a guard. LOG_FD doesn't initially exist, so (with or without the guard) we create an fd and populate LOG_FD, then everything continues as expected after that.
One purpose of the guard is to avoid unnecessarily allocating another fd some bashio script accidentally/incorrectly sources lib/log.sh twice. Once we have set up one LOG_FD, there is no need to set up another fd. While a single wasted fd (from sourcing exactly twice) would be basically harmless, a script that repeatedly sources lib/log.sh for some reason could cause a process to hit the open file descriptor limit and cause weird and hard-to-troubleshoot behavior. The guard prevents that from happening.
Another purpose of the guard is to prevent log messages and stdout from getting mixed if one bashio script runs another bashio script as a child (not as a subshell). Both the fd and the LOG_FD variable should be passed down to the child bashio script, enabling the child to log to the parent's stdout without interfering with the parent's ability to capture the child's stdout.
However, I just realized this case doesn't currently work because we are not exporting LOG_FD, so the child bashio script will get the fd but not the LOG_FD variable, and therefore will re-define LOG_FD and mix log messages and stdout again. I have opened #174 to fix that.

Finally, this statement is incorrect:

However, it seems that $() does not actually creates a subshell process if its a function (which it is in bashio::config case).

If you are looking at $$ then it might appear that $() doesn't create a subshell process if it is a function, but $BASHPID shows that it is a separate process.

Does that help explain how/why this works?

@agners
Copy link
Contributor

agners commented Sep 29, 2025

Finally, this statement is incorrect:

However, it seems that $() does not actually creates a subshell process if its a function (which it is in bashio::config case).
If you are looking at $$ then it might appear that $() doesn't create a subshell process if it is a function, but $BASHPID shows that it is a separate process.

It is not that I just made these statements up. I tested it:

diff --git a/lib/log.sh b/lib/log.sh
index 5f94701..9df3415 100644
--- a/lib/log.sh
+++ b/lib/log.sh
@@ -7,6 +7,8 @@
 # to be included in add-on scripts to reduce code duplication across add-ons.
 # ==============================================================================
 
+echo "Logging from PID $$"
+
 # Unless $LOG_FD is already set to a valid fd
 if ! [[ "${LOG_FD-}" =~ ^[0-9]+$ ]] || ! { : >&"${LOG_FD-2}"; } 2>/dev/null; then
   # Preserve the original STDOUT on a free fd (stored in $LOG_FD) so that we can
#!lib/bashio

echo "In PID $$"
echo $(bashio::log.info "Test")
$ lib/bashio test.sh
Logging from PID 13870
In PID 13870
[09:24:07] INFO: Test

Does that help explain how/why this works?

As I said initially, the I've rebuilt the NUT add-on without guard and export and it worked. Can you explain why it worked if my statements are not true?

@PaulSD
Copy link
Contributor Author

PaulSD commented Oct 7, 2025

@agners
(Sorry for the delay in responding - I've been unavailable for a bit)

It is not that I just made these statements up. I tested it:
+echo "Logging from PID $$"

From man bash, in reference to the $$ and BASH_PID special variables:

       $      Expands  to  the  process  ID  of  the  shell.  In a () subshell, it expands to the
              process ID of the current shell, not the subshell.
       BASHPID
              Expands  to the process ID of the current bash process.  This differs from $$ under
              certain circumstances, such as subshells that do not require bash to be re-initialized.

Test again using $BASHPID instead of $$ and you will see that $() does in fact create a subshell process.

As I said initially, the I've rebuilt the NUT add-on without guard and export and it worked. Can you explain why it worked if my statements are not true?

While your statement about $() running in the same process is false, your other statements are true.

So, the reason it works without the guard and export is a combination of:

  • (as you said) a subshell inherits the environment variable even without exporting
  • (as I said) file handles/descriptors created in BASH are inherited by / exported to commands run by bash (including but not limited to subshells)
  • (as I said) the simple case works fine without a guard. LOG_FD doesn't initially exist, so (with or without the guard) we create an fd and populate LOG_FD, then everything continues as expected after that.

@agners
Copy link
Contributor

agners commented Oct 8, 2025

Test again using $BASHPID instead of $$ and you will see that $() does in fact create a subshell process.

$ git diff
diff --git a/lib/log.sh b/lib/log.sh
index 5f94701..c296537 100644
--- a/lib/log.sh
+++ b/lib/log.sh
@@ -7,6 +7,8 @@
 # to be included in add-on scripts to reduce code duplication across add-ons.
 # ==============================================================================
 
+echo "Logging from PID $BASHPID"
+
 # Unless $LOG_FD is already set to a valid fd
 if ! [[ "${LOG_FD-}" =~ ^[0-9]+$ ]] || ! { : >&"${LOG_FD-2}"; } 2>/dev/null; then
   # Preserve the original STDOUT on a free fd (stored in $LOG_FD) so that we can
$ cat test.sh 
#!lib/bashio

echo "In PID $BASHPID"
echo $(bashio::log.info "Test")
$ lib/bashio test.sh
Logging from PID 634993
In PID 634993
[09:59:20] INFO: Test

I read up a bit, and it seems that bash does some optimizations for subshells, some of which avoid forking if not necessary (and execute the function still in a "subshell" with its own environment, e.g. from CHANGES: Bash optimizes away more forks in subshells.). I guess we hit that optimization in our case.

But yeah, with that in mind its maybe safer to not rely on that optimization, and pass down LOG_FD to bashio subshells just in case.

@lmagyar
Copy link
Contributor

lmagyar commented Nov 5, 2025

Can someone please explain what shebang should we use in scripts in /usr/bin to make it work? Currently these scripts either see the log in the captured stdout of a bashio function, or can't see bashio functions at all.

@PaulSD
Copy link
Contributor Author

PaulSD commented Nov 5, 2025

(I replied in #177)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants