Installer fails with "qoderwake not found in qoderwake package" on hosts without bash process substitution (Termux/proot, some containers)
Summary
The install.sh installer aborts at the "locate binary inside archive" step on environments where bash process substitution <(...) is unavailable (e.g. Termux proot-distro, and some minimal containers). The error message (qoderwake not found in qoderwake package) is misleading: the qoderwake binary is present in the downloaded archive and its checksum matches the manifest. The real cause is that find_binary() consumes find's output via done < <(find …), and on these hosts the process-substitution file descriptor (/dev/fd/63) cannot be created, so find_binary always returns empty and the install dies.
Environment
|
|
| OS |
Debian 13 (trixie) under Termux proot-distro (Android) |
| Arch |
aarch64 |
| Kernel |
Linux 6.17.0-PRoot-Distro |
| Shell |
bash 5.x |
| qoderwake |
0.2.8 (qodercli-wake 1.1.8) |
| Installer URL |
https://download.qoder.com/qoderwake/install.sh |
Reproduce
curl -fsSL https://download.qoder.com/qoderwake/install.sh | bash
Output:
==> Verifying qoderwake checksum
main: line 432: /dev/fd/63: No such file or directory
Error: qoderwake not found in qoderwake package
find: 'standard output': Broken pipe
find: write error
Root cause
On the affected host /dev/fd exists but bash cannot create the high-numbered fds that <(...) relies on:
$ bash -c 'while IFS= read -r x; do echo "$x"; done < <(printf hello)'
bash: line 1: /dev/fd/63: No such file or directory
find_binary() (install.sh, ~L432) uses exactly that construct:
while IFS= read -r candidate; do
printf '%s\n' "$candidate"
return 0
done < <(find "$dir" -type f -name "$binary_name" -print) # <-- L440
When /dev/fd/63 can't be opened, the while body never executes, find_binary falls through to return 1, and the caller (~L523) runs die "$binary_name not found in $label package". The now-orphaned find then writes to the closed pipe, producing the Broken pipe lines. Note: command substitution $(…), pipes, and here-strings <<< all work fine on this host — only process substitution is broken.
The binary is actually present
$ curl -fsSL …/releases/0.2.8/qoderwake_0.2.8_linux_arm64.tar.gz -o qw.tar.gz
$ sha256sum qw.tar.gz
cc4a36f884e875226d6d94c18682a379569b49418585933f484943f05e60b737 qw.tar.gz # matches manifest
$ tar -tzf qw.tar.gz | head
qoderwake
qodercli/
qodercli/qodercli-wake
resources/
…
So this is an installer portability bug, not a packaging bug.
All affected process-substitution sites
grep -nE 'done < <\(' install.sh:
- L440, L446 —
find_binary() (critical path; this is what fails)
- L1012 —
parse_manifest_with_shell() (manifest JSON fallback; only reached when jq, python3, and node are all absent)
- L1090 —
list_manifest_platforms() (same fallback path)
Suggested fix
Replace process substitution with command substitution (or a pipe). find_binary():
find_binary() {
local dir="$1"
local binary_name="$2"
local candidate
- while IFS= read -r candidate; do
- printf '%s\n' "$candidate"
- return 0
- done < <(find "$dir" -type f -name "$binary_name" -print)
+ candidate="$(find "$dir" -type f -name "$binary_name" -print | head -n1)"
+ if [[ -n "$candidate" ]]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
if [[ "$binary_name" == "qodercli" ]]; then
- while IFS= read -r candidate; do
- printf '%s\n' "$candidate"
- return 0
- done < <(find "$dir" -type f \( -name "qoderclicn" -o -name "qoderclicn.exe" \) -print)
+ candidate="$(find "$dir" -type f \( -name "qoderclicn" -o -name "qoderclicn.exe" \) -print | head -n1)"
+ if [[ -n "$candidate" ]]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
fi
return 1
}
Using $(…) (rather than piping into the while) keeps return in the current shell, avoiding the subshell-return pitfall a pipe would introduce. For the L1012/L1090 manifest fallbacks, done < <(printf … | sed …) can become done <<< "$(printf … | sed …)" (here-strings work here) or a plain pipe.
I verified this end-to-end: with the patch, find_binary resolves …/qoderwake, the installer completes, qoderwake --version → 0.2.8, login succeeds, and qoderwake status reports the daemon online.
Workaround (for affected users now)
Patch the downloaded script before running:
curl -fsSL https://download.qoder.com/qoderwake/install.sh -o install.sh
# in find_binary(), replace the two `done < <(find …)` blocks with the command-substitution form above
bash install.sh
Secondary note: daemon start on hosts without systemd
Same class of host, separate code path: qoderwake start / restart shell out to systemctl daemon-reload, which exits non-zero on systemd-less hosts and aborts the daemon start. qoderwake start --no-keepalive works correctly as the no-systemd path. It would help to either document this or auto-detect command -v systemctl and fall back to --no-keepalive.
Installer fails with "qoderwake not found in qoderwake package" on hosts without bash process substitution (Termux/proot, some containers)
Summary
The
install.shinstaller aborts at the "locate binary inside archive" step on environments where bash process substitution<(...)is unavailable (e.g. Termuxproot-distro, and some minimal containers). The error message (qoderwake not found in qoderwake package) is misleading: theqoderwakebinary is present in the downloaded archive and its checksum matches the manifest. The real cause is thatfind_binary()consumesfind's output viadone < <(find …), and on these hosts the process-substitution file descriptor (/dev/fd/63) cannot be created, sofind_binaryalways returns empty and the install dies.Environment
proot-distro(Android)https://download.qoder.com/qoderwake/install.shReproduce
curl -fsSL https://download.qoder.com/qoderwake/install.sh | bashOutput:
Root cause
On the affected host
/dev/fdexists but bash cannot create the high-numbered fds that<(...)relies on:find_binary()(install.sh, ~L432) uses exactly that construct:When
/dev/fd/63can't be opened, thewhilebody never executes,find_binaryfalls through toreturn 1, and the caller (~L523) runsdie "$binary_name not found in $label package". The now-orphanedfindthen writes to the closed pipe, producing theBroken pipelines. Note: command substitution$(…), pipes, and here-strings<<<all work fine on this host — only process substitution is broken.The binary is actually present
So this is an installer portability bug, not a packaging bug.
All affected process-substitution sites
grep -nE 'done < <\(' install.sh:find_binary()(critical path; this is what fails)parse_manifest_with_shell()(manifest JSON fallback; only reached whenjq,python3, andnodeare all absent)list_manifest_platforms()(same fallback path)Suggested fix
Replace process substitution with command substitution (or a pipe).
find_binary():find_binary() { local dir="$1" local binary_name="$2" local candidate - while IFS= read -r candidate; do - printf '%s\n' "$candidate" - return 0 - done < <(find "$dir" -type f -name "$binary_name" -print) + candidate="$(find "$dir" -type f -name "$binary_name" -print | head -n1)" + if [[ -n "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi if [[ "$binary_name" == "qodercli" ]]; then - while IFS= read -r candidate; do - printf '%s\n' "$candidate" - return 0 - done < <(find "$dir" -type f \( -name "qoderclicn" -o -name "qoderclicn.exe" \) -print) + candidate="$(find "$dir" -type f \( -name "qoderclicn" -o -name "qoderclicn.exe" \) -print | head -n1)" + if [[ -n "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi fi return 1 }Using
$(…)(rather than piping into thewhile) keepsreturnin the current shell, avoiding the subshell-return pitfall a pipe would introduce. For the L1012/L1090 manifest fallbacks,done < <(printf … | sed …)can becomedone <<< "$(printf … | sed …)"(here-strings work here) or a plain pipe.I verified this end-to-end: with the patch,
find_binaryresolves…/qoderwake, the installer completes,qoderwake --version→0.2.8, login succeeds, andqoderwake statusreports the daemon online.Workaround (for affected users now)
Patch the downloaded script before running:
curl -fsSL https://download.qoder.com/qoderwake/install.sh -o install.sh # in find_binary(), replace the two `done < <(find …)` blocks with the command-substitution form above bash install.shSecondary note: daemon start on hosts without systemd
Same class of host, separate code path:
qoderwake start/restartshell out tosystemctl daemon-reload, which exits non-zero on systemd-less hosts and aborts the daemon start.qoderwake start --no-keepaliveworks correctly as the no-systemd path. It would help to either document this or auto-detectcommand -v systemctland fall back to--no-keepalive.