mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-18 15:47:37 +02:00
Compare commits
2 Commits
fc07a2d1b4
...
create-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc33543d4b | ||
|
|
df62d357d8 |
@@ -4,30 +4,29 @@ description: >-
|
||||
Workflow for alexbelgium/hassio-addons Home Assistant add-on work: diagnose with real
|
||||
measurements, independent Codex review, implement, open a PR, resolve CodeRabbit / Copilot /
|
||||
Codex bot review comments, verify in production. Use for any task touching an add-on in this
|
||||
repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, build.json,
|
||||
updater.json, cont-init.d, services.d or s6 changes, version and CHANGELOG bumps, a failing
|
||||
add-on CI check, opening or iterating PRs — and, on an add-on task, when asked to "check with
|
||||
codex", "verify with chatgpt", or resolve bot comments. Cheap for small asks: a light path skips
|
||||
the heavy steps.
|
||||
repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, cont-init.d or s6 changes,
|
||||
version bumps, opening or iterating PRs — and when asked to "check with codex", "verify with
|
||||
chatgpt", or resolve bot comments. Cheap for small asks: a light path skips the heavy steps.
|
||||
---
|
||||
|
||||
# Home Assistant add-on workflow
|
||||
|
||||
**Answer style.** Chat replies are terse — no pleasantries, tool-call narration, decorative tables,
|
||||
emoji or dumped logs; quote the shortest decisive line and don't re-print what is already in
|
||||
context. Telegraphic fragments are fine *there*. Two things outrank brevity, because dropping a
|
||||
word from either changes the meaning rather than shortening it: never compress uncertainty markers
|
||||
("likely", "assumed", "not verified"), negations, numbers, units, technical terms, code or error
|
||||
strings — step 9's Verified/Checked/Assumed distinction wins every time; and write full prose
|
||||
wherever a fragment could be read two ways — security warnings, irreversible-action confirmations,
|
||||
multi-step sequences, and everything persisted (commits, CHANGELOG entries, PR bodies, review-thread
|
||||
replies, the step 10 report).
|
||||
**Answer style.** Chat replies are terse: no pleasantries, no tool-call narration, no decorative
|
||||
tables or emoji, no dumped logs — quote the shortest decisive line, and don't re-read or re-print
|
||||
what is already in context. Fragments and dropped articles are fine. Never compressed: uncertainty
|
||||
markers ("likely", "assumed", "not verified"), negations (`not`/`never`/`no`/`only`), numbers,
|
||||
units, technical terms, code blocks, error strings — step 9's Verified/Checked/Assumed distinction
|
||||
outranks brevity every time. Write in full prose, not fragments, for security warnings,
|
||||
irreversible-action confirmations, and any multi-step sequence a fragment could make ambiguous.
|
||||
Persisted text is prose too: commits, CHANGELOG entries, PR bodies, review-thread replies, the
|
||||
step 10 report.
|
||||
|
||||
Triage first, then one of two paths:
|
||||
|
||||
- **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels
|
||||
1-3 (below), simple questions: scope → implement → validate (step 4) → PR (version bump +
|
||||
CHANGELOG still required) → resolve bot comments.
|
||||
1-3 (below), simple questions: scope → implement → validate (`$SKILL/scripts/validate.sh
|
||||
<addon> --vs-master`; `$SKILL` defined below) → PR (version bump + CHANGELOG still required) →
|
||||
resolve bot comments.
|
||||
- **Full loop** — performance/RAM/CPU work, diagnosis, anything changing a shipped default,
|
||||
ladder levels 4-6, or an explicit Codex-check request: scope → measure → plan → Codex reviews
|
||||
the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve
|
||||
@@ -48,8 +47,9 @@ reasoning about a hypothetical *host* either. A defensive branch is complexity l
|
||||
the input that reaches it and the image or host where that happens, or delete it and let the case
|
||||
fail visibly instead.
|
||||
|
||||
**Skill root.** The canonical copy lives at `.claude/skills/hassio-addon-workflow/`. Set it once;
|
||||
every `scripts/…` and `references/…` path below is relative to it:
|
||||
**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is
|
||||
checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once,
|
||||
then every `scripts/…` and `references/…` path below is relative to it:
|
||||
|
||||
```bash
|
||||
SKILL="$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow"
|
||||
@@ -63,10 +63,9 @@ bash "$SKILL/scripts/preflight.sh" # and likewise for the other scripts
|
||||
|
||||
**Delegate heavy output to a subagent.** Codex reviews and multi-thread PR triage produce output
|
||||
you don't need verbatim in your own context. For Codex's plan review (step 3), Codex's code
|
||||
review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): if subagents
|
||||
are available, launch one to run the command and report back only the objections/findings and your
|
||||
assessment of each, not the raw transcript. Otherwise redirect the output to a file and read the
|
||||
parts you need.
|
||||
review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): launch a
|
||||
subagent to run the command and report back only the objections/findings and your assessment of
|
||||
each, not the raw transcript.
|
||||
|
||||
---
|
||||
|
||||
@@ -86,11 +85,9 @@ Measure the running add-on rather than reasoning from source (`$BUILD_VERSION` s
|
||||
- Is this flag/driver/package actually present? → inspect the artifact: `/proc/<pid>/cmdline`,
|
||||
`command -v`, `/var/log/apt/history.log`
|
||||
|
||||
Verify you're reading the right revision first — `scripts/preflight.sh` compares the checkout
|
||||
against the running `$BUILD_VERSION`, which catches the usual stale branch before it costs a full
|
||||
analysis pass (a version match does not prove the source is identical). Read
|
||||
`references/evidence.md` before interpreting any number you did not get straight from
|
||||
`measure.sh`, and for the failure modes this step exists to prevent.
|
||||
Verify you're reading the right revision first — `scripts/preflight.sh` catches a stale branch
|
||||
before it costs a full analysis pass. Measurement methodology, gotchas, and real failure examples:
|
||||
`references/evidence.md`.
|
||||
|
||||
## 3. Plan — choose the mechanism level, then Codex reviews it (full loop)
|
||||
|
||||
@@ -120,60 +117,45 @@ Attack your own plan before implementing:
|
||||
- What am I **inferring** that I could instead **detect at runtime** or **record explicitly**?
|
||||
Highest-yield question here — see `references/evidence.md`'s failure-mode section.
|
||||
- For every branch that exists **only to survive something going wrong**: name the image or host
|
||||
where that input arrives (the standing rule above) and go and look, before you write it.
|
||||
where that input actually arrives, and go and look. Naming is the bar, not reproducing it here —
|
||||
`references/simplify.md` works the `/dev/shm` guard and the `s6-dumpenv` fallback through that
|
||||
distinction.
|
||||
|
||||
Full loop only, before writing code: get Codex's independent read on the plan, delegated as above —
|
||||
`references/codex-review.md` has the invocation and how to write the prompt.
|
||||
Full loop only, before writing code: get Codex's independent read on the plan. Spawn a subagent
|
||||
whose prompt includes the path `references/codex-review.md` and tells it to follow that file's
|
||||
invocation, then report back only Codex's objections and an assessment of each — not the raw
|
||||
transcript.
|
||||
|
||||
## 4. Implement
|
||||
|
||||
Read the `references/traps.md` section matching what you're about to touch. It is ~18 KB and all
|
||||
but one section is irrelevant to any given edit, so print the one you need rather than reading the
|
||||
file — run it with no argument to list the sections:
|
||||
|
||||
| Touching | Run |
|
||||
| --- | --- |
|
||||
| an option or anything a base-image service reads | `bash "$SKILL/scripts/traps.sh" passing` |
|
||||
| a file the app also writes itself | `bash "$SKILL/scripts/traps.sh" "app's own"` |
|
||||
| shell, bashio, a symlinked script | `bash "$SKILL/scripts/traps.sh" bashio` |
|
||||
| `Dockerfile`, `build.json`, an arch guard | `bash "$SKILL/scripts/traps.sh" dockerfile` |
|
||||
| Chromium, Electron, Xvfb | `bash "$SKILL/scripts/traps.sh" chromium` |
|
||||
|
||||
Then validate with `scripts/validate.sh <addon> --vs-master`, and write behavioural tests for
|
||||
anything with branches, targeting **the regression a reviewer described**, not just the happy
|
||||
path.
|
||||
Touching a shell script, Dockerfile, or env option? Read `references/traps.md` first — skim the
|
||||
headings, read the sections you're about to touch; the bashio, s6-env, arch-guard and versioning
|
||||
traps are all live. (The light-path facts it holds — versioning format, CHANGELOG heading — are
|
||||
already inline in step 7.) Validate with `scripts/validate.sh <addon> --vs-master`. Write
|
||||
behavioural tests for anything with branches, targeting **the regression a reviewer described**,
|
||||
not just the happy path.
|
||||
|
||||
## 5. Simplify
|
||||
|
||||
Six questions over your own diff, before anyone else reads it:
|
||||
|
||||
- **Level** — did the diff stay at the ladder level chosen in step 3, or creep up one?
|
||||
- **Deletion** — can this be solved by deleting instead of adding?
|
||||
- **Size** — is the fix bigger than the thing it fixes?
|
||||
- **Reuse** — does any hunk reimplement what `.templates/`, another script in this add-on, or a
|
||||
sibling add-on already does? If a future add-on hits this problem, will it find one way to solve
|
||||
it or two? Fold near-duplicates in, or justify the divergence in the PR body.
|
||||
- **Depth** — is this a special case bolted onto shared infrastructure? Fix the shared mechanism
|
||||
instead once more than one add-on hits it; generalising from a single case is how bespoke
|
||||
designs get built, so below that bar the special case is the right call.
|
||||
- **Longevity** — how does this fail in three years, when the base image or upstream has moved?
|
||||
|
||||
The standing exception to Reuse and Depth: scripts shared by symlink with the webtop add-ons take a
|
||||
new numbered script, never an edit (`references/traps.md#shell-and-bashio`). Case studies for the rest, including
|
||||
what shipped when this pass was skipped: `references/simplify.md`.
|
||||
Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this
|
||||
be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in
|
||||
three years? And on reuse: does any hunk reimplement something `.templates/`, another script in
|
||||
this add-on, or a sibling add-on already does — and if a future add-on hits this same problem,
|
||||
will it find one way to solve it or two? Fold a near-duplicate into the existing mechanism, or
|
||||
justify the divergence in the PR body — but never at the cost of an isolation rule
|
||||
`references/traps.md` documents: scripts shared by symlink with the webtop add-ons take a new
|
||||
numbered script, not an edit. Case studies of what happens when this check is skipped:
|
||||
`references/simplify.md`.
|
||||
|
||||
## 6. Codex attacks the code, then simplify what the review added (full loop only)
|
||||
|
||||
Same delegated invocation, pointed at `git diff origin/master...HEAD` plus your reasoning per
|
||||
hunk. Details in `references/codex-review.md`.
|
||||
|
||||
Then run step 5's checks again over the hunks the review changed. Adversarial review mostly argues
|
||||
*for* another branch — that is what it is asked to do — so accepting objections tends to ratchet
|
||||
the diff upward, and nothing else in the loop walks it back down. Sort each objection before you
|
||||
write anything:
|
||||
**"this is wrong"** is a bug and you fix it; **"this is undefended"** is a claim about some host,
|
||||
and it needs the same demonstration you would demand of a measurement — is the case it defends one
|
||||
you have now demonstrated, or one you have merely been told about? Taking a
|
||||
Then run step 5's checks again over the hunks the review changed. Adversarial review only ever
|
||||
argues *for* another branch — that is its job — so accepting objections ratchets the diff upward,
|
||||
and nothing else in the loop walks it back down. For each accepted objection: is the case it
|
||||
defends one you have now demonstrated, or one you have merely been told about? Taking a
|
||||
correctness objection often deletes the code that made it necessary, and a fix that collapses back
|
||||
to fewer lines than you started the review with is the normal outcome, not a suspicious one.
|
||||
|
||||
@@ -183,23 +165,20 @@ kind of edit that leaves a stray `fi` behind.
|
||||
|
||||
## 7. Open the PR
|
||||
|
||||
Three hard gates — **`CHANGELOG.md` updated**, the **HA add-on linter**
|
||||
(`frenck/action-addon-linter`), and the **add-on image build** — but only on a PR that changes a
|
||||
top-level `config.*`. On a PR that doesn't (docs, `.github/`, `.claude/`) they *skip*, which is not
|
||||
the same as passing. Super-Linter runs on every PR and is `continue-on-error`, so it never blocks;
|
||||
fix its real findings anyway. Nothing checks the version bump, so bump it yourself — Supervisor
|
||||
won't offer a rebuild without one, and `CLAUDE.md` has the format. Update `README.md` if you added
|
||||
options; write the CHANGELOG heading as `## <version> (<date>)`, matching the date format already
|
||||
in that file — almost always ISO `YYYY-MM-DD`, see `references/traps.md#ci-and-review-bots`.
|
||||
CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter**
|
||||
(`frenck/action-addon-linter`, blocking — not the weekly Super-Linter, which is non-blocking), and
|
||||
the **add-on image build**. Bump `version` anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see
|
||||
`references/traps.md#versioning`) — Supervisor won't offer a rebuild without it. Update
|
||||
`README.md` if you added options; write the CHANGELOG heading as `## <version> (<date>)`,
|
||||
matching the date format already in that file — almost always ISO `YYYY-MM-DD`, see
|
||||
`references/traps.md#ci-and-review-bots`.
|
||||
|
||||
Write the body to a file, `gh pr create --body-file`: state what was measured, what changed,
|
||||
**what is not verified**, and how to roll back the riskiest hunk alone.
|
||||
|
||||
## 8. Resolve review comments
|
||||
|
||||
`scripts/pr_review.sh list|status|watch <PR>` to read, `reply <PR> <COMMENT_ID> <text|@file>` and
|
||||
`resolve <PR> <THREAD_ID…|--all>` to answer; run it with no arguments for the full usage. For every
|
||||
comment, **reproduce the
|
||||
`scripts/pr_review.sh list|reply|resolve|status|watch <PR>`. For every comment, **reproduce the
|
||||
claim before agreeing or disagreeing** — reviewers are frequently right and occasionally
|
||||
confidently wrong; a reproduction takes a minute and decides it either way. Reply with the
|
||||
evidence, then resolve. **Push back when you're right**, on the thread — a resolved-but-wrong
|
||||
@@ -213,12 +192,13 @@ work" — either it was exercised, or say plainly it wasn't.
|
||||
|
||||
Light path: verification is `validate.sh` plus CI; anything beyond that is Assumed. Full loop: CI
|
||||
passing proves the build works, not that the change does anything — re-run the measurement that
|
||||
motivated the work once the rebuilt add-on is running. Real "merged and inert" examples, and what
|
||||
to do when a fix cannot be self-verified: `references/evidence.md`. Then confirm the change
|
||||
survived the merge: `git fetch origin master` first (the tracking ref is stale otherwise), then
|
||||
`git diff origin/master -- <the paths you touched>` must come back empty. Ancestry is not the
|
||||
check, and the builder reverts merges for reasons unrelated to your diff — both explained in
|
||||
`references/traps.md#ci-and-review-bots`.
|
||||
motivated the work once the rebuilt add-on is running. After merge, `git fetch origin master`
|
||||
(the tracking ref is stale otherwise), then confirm the *changes* survived — `git diff
|
||||
origin/master -- <the paths you touched>` comes back empty. Ancestry is not the check: a revert
|
||||
leaves your commit in history and undoes its tree, so `--contains` reports success either way. The
|
||||
builder's revert-on-failure job can revert a merge for reasons unrelated to your diff (see
|
||||
`references/traps.md#ci-and-review-bots`). Real "merged and inert" examples, and what
|
||||
to do when a fix can't be self-verified: `references/evidence.md`.
|
||||
|
||||
## 10. Calibrate and report
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
Used for step 3 (plan review) and step 6 (code review), full loop only. Codex is a genuinely
|
||||
different model reading the files itself; on this workload it has repeatedly been worth the
|
||||
minutes. Run it through a subagent, per SKILL.md's delegation note.
|
||||
minutes. Delegate the invocation to a subagent (see SKILL.md's subagent-delegation note) so its
|
||||
output doesn't land verbatim in your context — have the subagent return only Codex's objections
|
||||
and your assessment of each.
|
||||
|
||||
## Invocation
|
||||
|
||||
@@ -10,12 +12,11 @@ minutes. Run it through a subagent, per SKILL.md's delegation note.
|
||||
on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. The MCP tool is still fine
|
||||
for short questions.
|
||||
|
||||
`--sandbox read-only` blocks writes, not reads, and `approval_policy=never` stops it asking for
|
||||
permission rather than stopping it acting — so it can still run read-only commands and often
|
||||
falls back to fetching the repo from GitHub instead of reading your worktree. Paste every number
|
||||
into the prompt rather than expecting it to gather them, and treat what it reports about *local*
|
||||
state as unverified. `- <` feeds the prompt file on stdin. Run it in the background so you are not
|
||||
blocked for the several minutes it takes (`&` here, or your harness's background-task mechanism):
|
||||
`--sandbox read-only` lets Codex read files but blocks writes and command execution, and
|
||||
`approval_policy=never` means it will not be prompted for permission to run anything either — so
|
||||
paste every number into the prompt rather than expecting Codex to gather it. `- <` feeds the
|
||||
prompt file on stdin. Run it in the background so you are not blocked for the several minutes it
|
||||
takes (`&` here, or your harness's background-task mechanism):
|
||||
|
||||
```bash
|
||||
codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
@@ -39,5 +40,9 @@ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
confidently, and separately caught a genuine methodology error in the same review. Treat its
|
||||
confirmations with the same scepticism as its objections — especially about the build.
|
||||
|
||||
**Its objections only ever argue for more code** — it is asked what could go wrong, never whether
|
||||
the branch it wants is reachable. Sort them before writing anything; SKILL.md step 6 is that pass.
|
||||
**Its objections ratchet complexity upward.** An adversarial reviewer is asked to find what could
|
||||
go wrong, so its output is a list of arguments for more code; it is never asked whether the branch
|
||||
it wants is reachable. Separate "this is wrong" from "this is undefended" before you write
|
||||
anything: the first is a bug and you fix it, the second is a claim about some host, and it needs
|
||||
the same demonstration you would demand of a measurement. That is what step 6's second simplify
|
||||
pass is for.
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
# Evidence — measurement methodology and case studies
|
||||
|
||||
## How to read the numbers
|
||||
## Why summed RSS and reserved-vs-resident both matter
|
||||
|
||||
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
|
||||
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
|
||||
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
|
||||
which completely changes which duplicate is worth removing. Quote private when arguing "removing
|
||||
this saves N MB".
|
||||
|
||||
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
|
||||
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
|
||||
|
||||
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
|
||||
Never attribute those totals to the add-on.
|
||||
|
||||
**A short CPU sample is not a CPU measurement.** A 3 s sample measured 2.3% where a 20 s sample
|
||||
measured 21.6% for the same process. Use >= 20 s for anything you report.
|
||||
|
||||
`scripts/measure.sh` already reports PSS and private alongside RSS, and resident separately from
|
||||
reserved, so these three only bite when you compute a figure yourself or quote one from `ps`.
|
||||
- **Summed RSS double-counts shared pages.** Removing a duplicate process frees its *private*
|
||||
memory, not its RSS. `scripts/measure.sh` reports PSS and private alongside RSS — quote
|
||||
**private** when arguing "removing this saves N MB".
|
||||
- **A big mapping is not necessarily resident.** Large SysV/tmpfs segments are lazily populated;
|
||||
reserved size is reported separately from resident for this reason.
|
||||
- `/proc/meminfo` and `free` show **host** figures (no memory cgroup namespace here) — never
|
||||
attribute those to the add-on.
|
||||
- Sample duration matters: a 3 s CPU sample measured 2.3% where a 20 s sample measured 21.6% for
|
||||
the same process. Use ≥20 s for anything you report.
|
||||
|
||||
## Before asserting anything, ask what would show it false
|
||||
|
||||
|
||||
@@ -30,24 +30,28 @@ Being able to build the complicated thing is not a reason to.
|
||||
It took the maintainer asking "is this the simplest way possible?" to run the pass that step 6
|
||||
now requires.
|
||||
|
||||
## Where SKILL.md step 5's questions get hard
|
||||
## Checks worth running against your own diff
|
||||
|
||||
**Deletion is not automatically the safe direction.** The `/dev/shm` case in `evidence.md` is a
|
||||
removal that reintroduced a crash loop on hosts unlike this one. A removal that depends on a host
|
||||
default needs the same verification as an addition.
|
||||
|
||||
**Naming the case is the bar for a defensive branch, not reproducing it.** Docker's 64 MB
|
||||
`/dev/shm` default is documented behaviour that Home Assistant does not override, so that guard
|
||||
stays even though this host measured 7.7 GB. Nobody could name a single image shipping
|
||||
`with-contenv` without `s6-dumpenv`, so that fallback went. If you cannot name the case, delete the
|
||||
branch: the situation then fails the way it already fails today, visibly, instead of through a
|
||||
second path that is never exercised and silently rots as the base images move. Weigh the cost both
|
||||
ways — a one-flag guard against a crash you cannot rule out is cheap; a second code path that
|
||||
degrades to the pre-fix behaviour anyway is not. Write in the PR body what you cut and why, so the
|
||||
next person does not re-add it from the same reasoning.
|
||||
|
||||
**"Bigger than the thing it fixes" is a smell, not a rule** — but it usually means the problem was
|
||||
framed one level too deep.
|
||||
|
||||
**Three-year failure** favours code that reads a documented knob. Code that reaches into private
|
||||
internals does not survive the base image moving.
|
||||
- **Did the diff stay at the ladder level chosen in step 3?** If it crept up a level, either
|
||||
justify that out loud or redo it at the level you chose.
|
||||
- **Can this be solved by deleting instead of adding?** A flag that shouldn't be passed, a
|
||||
process that shouldn't start, a registration that shouldn't be duplicated. Deleting usually
|
||||
shrinks the regression surface — but not always: the `/dev/shm` case in
|
||||
`references/evidence.md` is a removal that reintroduced a crash loop on hosts unlike this one.
|
||||
A removal that depends on a host default still needs the same verification as an addition.
|
||||
- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually
|
||||
means the problem was framed one level too deep.
|
||||
- **For each defensive branch: what input reaches it, on which image or host?** Go and check,
|
||||
the way you would check a measurement. The bar is being able to **name** the case, not to
|
||||
reproduce it here: Docker's 64 MB `/dev/shm` default is documented behaviour that HA does not
|
||||
override, so the bullet above keeps that guard even though this host measured 7.7 GB. Nobody
|
||||
could name a single image shipping `with-contenv` without `s6-dumpenv`, so that fallback went.
|
||||
If you cannot name the case, delete the branch — the situation then fails the way it already
|
||||
fails today, visibly, instead of through a second path that is never exercised and silently
|
||||
rots as the base images move. Weigh the cost too: a one-flag guard against a crash you cannot
|
||||
rule out is cheap, a second code path that degrades to the pre-fix behaviour anyway is not.
|
||||
Write down in the PR body what you cut and why, so the next person does not re-add it from the
|
||||
same reasoning.
|
||||
- **How does this fail in three years**, when the base image, Electron, or upstream has moved?
|
||||
Code that reads a documented knob keeps working. Code that reaches into private internals
|
||||
does not.
|
||||
|
||||
@@ -9,6 +9,7 @@ workflows and lint rules — that is not repeated here.
|
||||
## Contents
|
||||
|
||||
- [Environment and workspace](#environment-and-workspace)
|
||||
- [Measurement](#measurement)
|
||||
- [Passing values into base-image services](#passing-values-into-base-image-services)
|
||||
- [Writing into an app's own config](#writing-into-an-apps-own-config)
|
||||
- [Shell and bashio](#shell-and-bashio)
|
||||
@@ -45,6 +46,20 @@ gate. One observed run took ~3 hours, with 20+ runs queued against 2 executing
|
||||
runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in
|
||||
a background task, and never claim the build is verified when it hasn't run.
|
||||
|
||||
## Measurement
|
||||
|
||||
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
|
||||
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
|
||||
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
|
||||
which completely changes which duplicate is worth removing. Quote private when arguing "removing
|
||||
this saves N MB".
|
||||
|
||||
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
|
||||
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
|
||||
|
||||
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
|
||||
Never attribute those totals to the add-on.
|
||||
|
||||
**`rtk` filters some command output.** For a complete listing, redirect to a file and read that
|
||||
(`ps ... > $SP/ps.txt`), or use `rtk proxy <cmd>`.
|
||||
|
||||
@@ -175,9 +190,11 @@ build.
|
||||
|
||||
## Versioning
|
||||
|
||||
`CLAUDE.md` owns the format (`X.Y.Z.N`, never `X.Y.Z-N`, and why). The one thing it does not say:
|
||||
date-based versions (`2026.08.03`) are common here, so check whether master has already moved to
|
||||
the version you were about to use before you pick it.
|
||||
**`X.Y.Z.N`, never `X.Y.Z-N`.** A hyphen parses as a semver pre-release, which Supervisor treats
|
||||
as *older* than `X.Y.Z` — the update is never offered.
|
||||
|
||||
Date-based versions (`2026.08.03`) are common here. Check whether master has already moved to the
|
||||
version you were about to use.
|
||||
|
||||
## Chromium / Electron under Xvfb
|
||||
|
||||
|
||||
@@ -30,8 +30,14 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
|
||||
TARGET="${2:-}"
|
||||
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
|
||||
case "$VAR" in
|
||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;;
|
||||
*) echo "invalid env var name: $VAR" >&2; exit 1 ;;
|
||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || {
|
||||
echo "invalid env var name: $VAR" >&2
|
||||
exit 1
|
||||
} ;;
|
||||
*)
|
||||
echo "invalid env var name: $VAR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "== tracing ${VAR} =="
|
||||
@@ -39,7 +45,7 @@ echo
|
||||
|
||||
echo "1. /data/options.json (the user's saved options)"
|
||||
if [ -f /data/options.json ]; then
|
||||
python3 - "$VAR" <<'PY'
|
||||
python3 - "$VAR" << 'PY'
|
||||
import json, sys
|
||||
var = sys.argv[1]
|
||||
try:
|
||||
@@ -80,7 +86,8 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
|
||||
seen3=0
|
||||
for d in /var/run/s6/container_environment /run/s6/container_environment; do
|
||||
if [ -f "$d/$VAR" ]; then
|
||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
|
||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"
|
||||
seen3=1
|
||||
fi
|
||||
done
|
||||
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
|
||||
@@ -122,8 +129,8 @@ else
|
||||
done
|
||||
fi
|
||||
# What it was actually launched with beats any theory about its environment.
|
||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
|
||||
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \
|
||||
| grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -31,7 +31,7 @@ awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2>
|
||||
echo
|
||||
echo "== per-process memory (top 20 by PSS) =="
|
||||
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
|
||||
python3 - "$OUT" <<'PY'
|
||||
python3 - "$OUT" << 'PY'
|
||||
import os, sys
|
||||
rows = []
|
||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||
@@ -62,17 +62,17 @@ PY
|
||||
|
||||
echo
|
||||
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
|
||||
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||
ipcs -m 2> /dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||
|
||||
echo
|
||||
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
|
||||
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
|
||||
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
|
||||
# number rather than failing.
|
||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; }
|
||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2> /dev/null; }
|
||||
|
||||
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
|
||||
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100
|
||||
HZ=$(getconf CLK_TCK 2> /dev/null) && [ "$HZ" -gt 0 ] 2> /dev/null || HZ=100
|
||||
|
||||
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
|
||||
# and the busiest process is not necessarily a big one.
|
||||
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
|
||||
[ -r "/proc/$pid/stat" ] || continue
|
||||
t1=$(jiffies "$pid") || continue
|
||||
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
|
||||
delta=$(( t1 - ${t0[$pid]} ))
|
||||
delta=$((t1 - ${t0[$pid]}))
|
||||
[ "$delta" -gt 0 ] || continue
|
||||
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null)
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
|
||||
echo "$pct $pid $comm"
|
||||
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
|
||||
|
||||
echo
|
||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2> /dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
|
||||
echo " raw ps: $OUT/ps.txt"
|
||||
|
||||
@@ -5,14 +5,7 @@
|
||||
# Usage: preflight.sh [repo-path] [addon-slug]
|
||||
set -uo pipefail
|
||||
|
||||
# Default to the checkout this is run from, never a fixed path: a fixed path silently inspected
|
||||
# the main checkout while the caller worked in a worktree, reporting a branch nobody was editing —
|
||||
# the exact stale-checkout trap this script exists to catch. Falling back to one when git cannot
|
||||
# answer would recreate it, so refuse instead and make the caller say which repo they mean.
|
||||
REPO="${1:-}"
|
||||
if [ -z "$REPO" ]; then
|
||||
REPO=$(git rev-parse --show-toplevel 2> /dev/null) || REPO=""
|
||||
fi
|
||||
REPO="${1:-/data/claude/hassio-addons}"
|
||||
SLUG="${2:-}"
|
||||
|
||||
echo "== tools =="
|
||||
@@ -33,14 +26,9 @@ fi
|
||||
echo
|
||||
echo "== repo =="
|
||||
# git-aware check: in a worktree .git is a file, not a directory
|
||||
if [ -z "$REPO" ]; then
|
||||
echo " not inside a git checkout, and no repo path given"
|
||||
echo " -> pass one explicitly: preflight.sh <repo-path> [addon-slug]"
|
||||
exit 1
|
||||
fi
|
||||
if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo " no git repo at $REPO"
|
||||
exit 1
|
||||
exit 0
|
||||
fi
|
||||
cd "$REPO" || exit 0
|
||||
branch=$(git branch --show-current 2> /dev/null || echo "(detached)")
|
||||
@@ -59,7 +47,10 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
|
||||
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
|
||||
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
|
||||
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
|
||||
[ -f "$REPO/$cand/config.yaml" ] && {
|
||||
SLUG="$cand"
|
||||
break
|
||||
}
|
||||
done
|
||||
[ -z "$SLUG" ] && SLUG="$base"
|
||||
fi
|
||||
@@ -81,8 +72,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
else
|
||||
echo " MISMATCH — this branch is NOT what is running."
|
||||
git fetch origin master --quiet 2> /dev/null
|
||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
|
||||
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \
|
||||
| grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
echo " origin/master version = ${master:-unknown}"
|
||||
echo " -> work from origin/master; analysing this branch will mislead you."
|
||||
echo
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Print one section of references/traps.md.
|
||||
#
|
||||
# traps.md is ~18 KB and every section but one is irrelevant to any given edit: a shell fix needs
|
||||
# 642 bytes of it, a Dockerfile fix 2.6 KB, and "CI and review bots" — 35% of the file — is needed
|
||||
# at steps 7-8 and never at step 4. A markdown anchor cannot be loaded on its own, so reading the
|
||||
# file to reach one section pays for all of them. This prints just the section, so the routing
|
||||
# table in SKILL.md step 4 costs what it claims to.
|
||||
#
|
||||
# Usage: traps.sh <keyword> # substring of a section heading, case-insensitive
|
||||
# traps.sh # list the sections
|
||||
set -uo pipefail
|
||||
|
||||
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
|
||||
[ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; }
|
||||
|
||||
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
|
||||
list() {
|
||||
echo "sections (pass any substring):"
|
||||
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
|
||||
}
|
||||
|
||||
[ $# -eq 0 ] && { list; exit 0; }
|
||||
|
||||
# awk over exact heading text: section names contain '/' and other characters that would need
|
||||
# escaping in a sed address, and a keyword matching several headings should be an error, not a
|
||||
# silent pick of the first.
|
||||
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' |
|
||||
grep -iF -- "$1" | sed 's/^## //')
|
||||
|
||||
case "${#matches[@]}" in
|
||||
0)
|
||||
echo "no section matching '$1'" >&2
|
||||
list >&2
|
||||
exit 1
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
echo "'$1' matches ${#matches[@]} sections — be more specific:" >&2
|
||||
printf ' %s\n' "${matches[@]}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
awk -v want="## ${matches[0]}" '
|
||||
$0 == want { inside = 1; print; next }
|
||||
inside && /^## / { exit }
|
||||
inside { print }
|
||||
' "$TRAPS"
|
||||
@@ -13,22 +13,31 @@ set -uo pipefail
|
||||
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
|
||||
|
||||
ADDON="${1:-}"
|
||||
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
|
||||
[ "${ADDON:-}" = "--vs-master" ] && {
|
||||
ADDON=""
|
||||
set -- --vs-master
|
||||
}
|
||||
VS_MASTER=false
|
||||
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
||||
|
||||
if [ -z "$ADDON" ]; then
|
||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
|
||||
cut -d/ -f1 | sort -u | grep -vE '^\.' )
|
||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null \
|
||||
| cut -d/ -f1 | sort -u | grep -vE '^\.')
|
||||
if [ "${#_dirs[@]}" -gt 1 ]; then
|
||||
echo "several changed dirs: ${_dirs[*]}"
|
||||
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
|
||||
echo "pass one explicitly: validate.sh <addon-dir>"
|
||||
exit 1
|
||||
fi
|
||||
ADDON="${_dirs[0]:-}"
|
||||
fi
|
||||
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
|
||||
[ -z "$ADDON" ] && {
|
||||
echo "usage: validate.sh <addon-dir> [--vs-master]"
|
||||
exit 1
|
||||
}
|
||||
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
||||
echo "origin/master missing — run: git fetch origin master"; exit 1; }
|
||||
echo "origin/master missing — run: git fetch origin master"
|
||||
exit 1
|
||||
}
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
echo "== validating $ADDON =="
|
||||
|
||||
@@ -36,14 +45,22 @@ fail=0
|
||||
note() { printf ' %-13s %s\n' "$1" "$2"; }
|
||||
# execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither
|
||||
# linter below can read one, so anything either says about it is noise.
|
||||
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; }
|
||||
is_execline() {
|
||||
local l
|
||||
IFS= read -r l < "$1" 2> /dev/null
|
||||
[[ $l == '#!'*execlineb* ]]
|
||||
}
|
||||
|
||||
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both.
|
||||
files=()
|
||||
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
|
||||
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
||||
for f in "${files[@]}"; do
|
||||
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
|
||||
if ! out=$(bash -n "$f" 2>&1); then
|
||||
note "bash -n" "FAIL $f"
|
||||
echo "$out" | sed 's/^/ /'
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
|
||||
|
||||
@@ -65,13 +82,16 @@ command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
|
||||
|
||||
if [ -f "$ADDON/config.yaml" ]; then
|
||||
# path passed as argv, never interpolated into Python source
|
||||
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
|
||||
python3 - "$ADDON/config.yaml" << 'PY' || {
|
||||
import yaml,sys
|
||||
d=yaml.safe_load(open(sys.argv[1]))
|
||||
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
|
||||
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
|
||||
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
|
||||
PY
|
||||
note "config.yaml" "FAIL parse"
|
||||
fail=1
|
||||
}
|
||||
command -v yamllint > /dev/null 2>&1 && {
|
||||
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
||||
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
||||
@@ -79,7 +99,10 @@ PY
|
||||
fi
|
||||
|
||||
while IFS= read -r f; do
|
||||
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
|
||||
python3 -m py_compile "$f" 2> /dev/null || {
|
||||
note "py_compile" "FAIL $f"
|
||||
fail=1
|
||||
}
|
||||
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
||||
|
||||
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
||||
@@ -96,7 +119,8 @@ if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CH
|
||||
note "CHANGELOG" "updated"
|
||||
else
|
||||
# This one IS gated: onpr_check-pr.yaml exits 1 without it.
|
||||
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1
|
||||
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"
|
||||
fail=1
|
||||
fi
|
||||
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
|
||||
note "version" "bumped"
|
||||
@@ -110,7 +134,8 @@ note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only
|
||||
if $VS_MASTER; then
|
||||
echo
|
||||
echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
|
||||
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
added=0
|
||||
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
|
||||
# findings below could never reach $fail and the verdict would contradict the list.
|
||||
@@ -125,14 +150,26 @@ if $VS_MASTER; then
|
||||
# cancel it out and report a false clean.
|
||||
case "$f" in
|
||||
*.sh | *autostart | */run | */finish)
|
||||
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
|
||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
|
||||
command -v shellcheck > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (shellcheck not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; }
|
||||
;;
|
||||
*.yaml | *.yml)
|
||||
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
|
||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
|
||||
command -v yamllint > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (yamllint not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; }
|
||||
;;
|
||||
*Dockerfile)
|
||||
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
|
||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
|
||||
command -v hadolint > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (hadolint not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; }
|
||||
;;
|
||||
*) continue ;;
|
||||
esac
|
||||
b=$(cmd "$tmp/base" | sort)
|
||||
@@ -141,7 +178,8 @@ if $VS_MASTER; then
|
||||
[ -n "$new" ] && {
|
||||
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
|
||||
printf '%s\n' "$new" | sed 's/^/ /' | head -5
|
||||
added=1; fail=1
|
||||
added=1
|
||||
fail=1
|
||||
}
|
||||
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
|
||||
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"
|
||||
|
||||
2
.github/prompts/issue-classify.md
vendored
2
.github/prompts/issue-classify.md
vendored
@@ -40,7 +40,7 @@ the check is a guard, not a common case.)
|
||||
## Rule 1 — pick exactly one verdict
|
||||
|
||||
| verdict | when |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
|
||||
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
|
||||
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
|
||||
|
||||
2
.github/prompts/issue-fix.md
vendored
2
.github/prompts/issue-fix.md
vendored
@@ -39,7 +39,7 @@ rest are on you: nothing checks them, so breaking one ships silently.
|
||||
that value, and compare:
|
||||
|
||||
| `version` vs `U` | what to do | example |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
|
||||
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
|
||||
| anything else | **leave it alone** | plex, readarr, joal |
|
||||
|
||||
2
.github/scripts/ai_guard_paths.sh
vendored
2
.github/scripts/ai_guard_paths.sh
vendored
@@ -44,7 +44,7 @@ for pr in $PRS; do
|
||||
echo "$BAD"
|
||||
# Ensure the label exists before adding it — with set -e a missing
|
||||
# label would abort the whole loop and skip any PRs behind this one.
|
||||
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
gh label create "ai:blocked" --repo "$REPO" --color ededed > /dev/null 2>&1 || true
|
||||
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||
gh pr comment "$pr" --repo "$REPO" --body \
|
||||
|
||||
172
.github/scripts/ai_triage_context.sh
vendored
172
.github/scripts/ai_triage_context.sh
vendored
@@ -15,110 +15,116 @@ CTX="$OUT/context.md"
|
||||
: > "$CTX"
|
||||
|
||||
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
|
||||
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
|
||||
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
|
||||
|
||||
TITLE=$(jq -r '.title' "$OUT/issue.json")
|
||||
|
||||
# ---------------------------------------------------------------- addon slug
|
||||
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
|
||||
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1)
|
||||
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<< "$TITLE" | head -n1)
|
||||
ADDON=""
|
||||
if [ -n "$RAW" ]; then
|
||||
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
|
||||
# Directory list without checking out any of them.
|
||||
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
||||
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
||||
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
|
||||
done
|
||||
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
|
||||
# against a directory named calibre_web (underscore) matches neither exact
|
||||
# guess above, and would otherwise fall through to the substring fallback
|
||||
# below, which picks the shorter "calibre" instead — the wrong add-on.
|
||||
# Stripping -, _, . from both sides before comparing catches this case.
|
||||
if [ -z "$ADDON" ]; then
|
||||
CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND")
|
||||
while IFS= read -r dir; do
|
||||
if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi
|
||||
done < "$OUT/dirs.txt"
|
||||
fi
|
||||
# Last resort: longest directory name contained in the candidate.
|
||||
if [ -z "$ADDON" ]; then
|
||||
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
|
||||
CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW")
|
||||
# Directory list without checking out any of them.
|
||||
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
||||
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
||||
if grep -qxF "$guess" "$OUT/dirs.txt"; then
|
||||
ADDON="$guess"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
|
||||
# against a directory named calibre_web (underscore) matches neither exact
|
||||
# guess above, and would otherwise fall through to the substring fallback
|
||||
# below, which picks the shorter "calibre" instead — the wrong add-on.
|
||||
# Stripping -, _, . from both sides before comparing catches this case.
|
||||
if [ -z "$ADDON" ]; then
|
||||
CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND")
|
||||
while IFS= read -r dir; do
|
||||
if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then
|
||||
ADDON="$dir"
|
||||
break
|
||||
fi
|
||||
done < "$OUT/dirs.txt"
|
||||
fi
|
||||
# Last resort: longest directory name contained in the candidate.
|
||||
if [ -z "$ADDON" ]; then
|
||||
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
|
||||
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Issue #${ISSUE_NUMBER}"
|
||||
echo
|
||||
echo "Repo: ${REPO}"
|
||||
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
|
||||
echo
|
||||
echo "## Title"
|
||||
echo "$TITLE"
|
||||
echo
|
||||
echo "## Author"
|
||||
jq -r '.author.login' "$OUT/issue.json"
|
||||
echo
|
||||
echo "## Body"
|
||||
echo '```'
|
||||
jq -r '.body // "(empty)"' "$OUT/issue.json"
|
||||
echo '```'
|
||||
echo
|
||||
echo "## Existing comments (in order)"
|
||||
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
|
||||
echo
|
||||
echo "## Existing labels"
|
||||
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
|
||||
echo "# Issue #${ISSUE_NUMBER}"
|
||||
echo
|
||||
echo "Repo: ${REPO}"
|
||||
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
|
||||
echo
|
||||
echo "## Title"
|
||||
echo "$TITLE"
|
||||
echo
|
||||
echo "## Author"
|
||||
jq -r '.author.login' "$OUT/issue.json"
|
||||
echo
|
||||
echo "## Body"
|
||||
echo '```'
|
||||
jq -r '.body // "(empty)"' "$OUT/issue.json"
|
||||
echo '```'
|
||||
echo
|
||||
echo "## Existing comments (in order)"
|
||||
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
|
||||
echo
|
||||
echo "## Existing labels"
|
||||
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
|
||||
} >> "$CTX"
|
||||
|
||||
# ------------------------------------------------------------- addon sources
|
||||
if [ -n "$ADDON" ]; then
|
||||
{
|
||||
echo
|
||||
echo "## Addon files: ${ADDON}/"
|
||||
# `set` REPLACES the checkout list, so .templates has to be repeated here
|
||||
# or the workflow's sparse-checkout of it is silently undone at this point
|
||||
# — which is exactly the state that starved #2949 of its turn budget.
|
||||
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
|
||||
# Swallowing this used to leave ADDON resolved with no files behind it,
|
||||
# so the classifier could still reach high confidence off the addon
|
||||
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
||||
# its low-confidence check on.
|
||||
echo
|
||||
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
|
||||
else
|
||||
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
|
||||
[ -f "$ADDON/$f" ] || continue
|
||||
{
|
||||
echo
|
||||
echo "### ${ADDON}/${f}"
|
||||
echo '```'
|
||||
head -c 8000 "$ADDON/$f"
|
||||
echo '```'
|
||||
done
|
||||
echo "## Addon files: ${ADDON}/"
|
||||
# `set` REPLACES the checkout list, so .templates has to be repeated here
|
||||
# or the workflow's sparse-checkout of it is silently undone at this point
|
||||
# — which is exactly the state that starved #2949 of its turn budget.
|
||||
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
|
||||
# Swallowing this used to leave ADDON resolved with no files behind it,
|
||||
# so the classifier could still reach high confidence off the addon
|
||||
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
||||
# its low-confidence check on.
|
||||
echo
|
||||
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
|
||||
else
|
||||
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
|
||||
[ -f "$ADDON/$f" ] || continue
|
||||
echo
|
||||
echo "### ${ADDON}/${f}"
|
||||
echo '```'
|
||||
head -c 8000 "$ADDON/$f"
|
||||
echo '```'
|
||||
done
|
||||
|
||||
echo
|
||||
echo "## Recent commits touching ${ADDON}/"
|
||||
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true
|
||||
fi
|
||||
} >> "$CTX"
|
||||
echo
|
||||
echo "## Recent commits touching ${ADDON}/"
|
||||
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2> /dev/null || true
|
||||
fi
|
||||
} >> "$CTX"
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------- possible duplicates
|
||||
{
|
||||
echo
|
||||
echo "## Similar existing issues (candidate duplicates)"
|
||||
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
||||
# Excludes the issue being triaged: if it's already indexed by GitHub search
|
||||
# by the time this runs, keyword overlap with its own title would otherwise
|
||||
# list it as a "candidate duplicate" of itself.
|
||||
gh search issues --repo "$REPO" --limit 15 \
|
||||
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
|
||||
| jq -r --argjson self "$ISSUE_NUMBER" \
|
||||
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|
||||
|| echo "(search unavailable)"
|
||||
echo
|
||||
echo "## Similar existing issues (candidate duplicates)"
|
||||
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
||||
# Excludes the issue being triaged: if it's already indexed by GitHub search
|
||||
# by the time this runs, keyword overlap with its own title would otherwise
|
||||
# list it as a "candidate duplicate" of itself.
|
||||
gh search issues --repo "$REPO" --limit 15 \
|
||||
--json number,title,state,url -- "$KEYWORDS" 2> /dev/null \
|
||||
| jq -r --argjson self "$ISSUE_NUMBER" \
|
||||
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|
||||
|| echo "(search unavailable)"
|
||||
} >> "$CTX"
|
||||
|
||||
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"
|
||||
|
||||
2
.github/workflows/daily_ai_fix.yaml
vendored
2
.github/workflows/daily_ai_fix.yaml
vendored
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
- name: Analyse and fix
|
||||
if: steps.batch.outputs.count != '0'
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
||||
|
||||
2
.github/workflows/on_claude_mention.yml
vendored
2
.github/workflows/on_claude_mention.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||
|
||||
2
.github/workflows/on_issue_approved.yaml
vendored
2
.github/workflows/on_issue_approved.yaml
vendored
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Execute the plan
|
||||
if: steps.bundle.outputs.has_plan == 'true'
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
|
||||
2
.github/workflows/on_issues_ai_triage.yaml
vendored
2
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -166,7 +166,7 @@ jobs:
|
||||
id: classify
|
||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Without this the action falls back to the OIDC -> Claude App token
|
||||
|
||||
2
.github/workflows/on_pr_coderabbit.yml
vendored
2
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Address CodeRabbit comments
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
|
||||
@@ -6,7 +6,7 @@ set -e
|
||||
# Banner
|
||||
# ======================================================================
|
||||
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
# Source standalone bashio first to provide function definitions
|
||||
if [ -f /usr/local/lib/bashio-standalone.sh ]; then
|
||||
source /usr/local/lib/bashio-standalone.sh
|
||||
@@ -19,11 +19,11 @@ if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if [ ! -f /usr/bin/bashio ]; then
|
||||
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
|
||||
fi
|
||||
grep -rl "^#!.*bashio" /etc |
|
||||
while IFS= read -r f; do
|
||||
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
|
||||
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
|
||||
done
|
||||
grep -rl "^#!.*bashio" /etc \
|
||||
| while IFS= read -r f; do
|
||||
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
|
||||
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
|
||||
done
|
||||
else
|
||||
bashio::log.blue '-----------------------------------------------------------'
|
||||
bashio::log.blue " Add-on: $(bashio::addon.name)"
|
||||
@@ -49,7 +49,7 @@ bashio::log.green ' Provided by: https://github.com/alexbelgium/hassio-addons '
|
||||
bashio::log.blue '-----------------------------------------------------------'
|
||||
|
||||
# Adapt user abc
|
||||
if command -v id &>/dev/null && id abc &>/dev/null; then
|
||||
if command -v id &> /dev/null && id abc &> /dev/null; then
|
||||
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
|
||||
PUID="$(bashio::config "PUID")"
|
||||
PGID="$(bashio::config "PGID")"
|
||||
|
||||
@@ -152,15 +152,15 @@ if [[ "${1:-}" == "--self-test" ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
[[ "$self_test_rc" -eq 0 ]] &&
|
||||
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||
[[ "$self_test_rc" -eq 0 ]] \
|
||||
&& echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||
exit "$self_test_rc"
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Guard: only run inside Supervisor-managed add-ons
|
||||
################################################################################
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
echo "..."
|
||||
exit 0
|
||||
fi
|
||||
@@ -179,7 +179,7 @@ ENV_FILE="/.env"
|
||||
ETC_ENV_FILE="/etc/environment"
|
||||
|
||||
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
|
||||
command -v jq >/dev/null || bashio::exit.nok "jq is required"
|
||||
command -v jq > /dev/null || bashio::exit.nok "jq is required"
|
||||
|
||||
mkdir -p /etc
|
||||
touch "$ETC_ENV_FILE"
|
||||
@@ -193,7 +193,10 @@ SECRETSOURCE=""
|
||||
|
||||
resolve_secret() {
|
||||
local v="$1" name line
|
||||
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; }
|
||||
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || {
|
||||
printf '%s' "$v"
|
||||
return
|
||||
}
|
||||
name="${BASH_REMATCH[1]}"
|
||||
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
set -e
|
||||
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
bashio::log.blue "Disabled : please use another method"
|
||||
exit 0
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
bashio::log.blue "Disabled : please use another method"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons"
|
||||
@@ -16,167 +16,167 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins
|
||||
####################
|
||||
|
||||
cleanup_cred() {
|
||||
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
|
||||
rm -f "$CRED_FILE" || true
|
||||
fi
|
||||
CRED_FILE=""
|
||||
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
|
||||
rm -f "$CRED_FILE" || true
|
||||
fi
|
||||
CRED_FILE=""
|
||||
}
|
||||
|
||||
test_mount() {
|
||||
MOUNTED=false
|
||||
ERROR_MOUNT=false
|
||||
mountpoint="/mnt/$diskname"
|
||||
|
||||
if ! mountpoint -q "$mountpoint"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_test_write() {
|
||||
local testfile="$mountpoint/.writetest_$$"
|
||||
if : >"$testfile" 2>/dev/null; then
|
||||
rm -f "$testfile" 2>/dev/null || true
|
||||
return 0
|
||||
else
|
||||
rm -f "$testfile" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if _test_write 2>/dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local orig_mountoptions="$MOUNTOPTIONS"
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
|
||||
echo "... retrying mount with noserverino"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noserverino"
|
||||
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2>/dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
|
||||
echo "... retrying mount with noperm"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noperm"
|
||||
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2>/dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if mountpoint -q "$mountpoint"; then
|
||||
MOUNTED="readonly"
|
||||
else
|
||||
MOUNTED=false
|
||||
fi
|
||||
return 0
|
||||
ERROR_MOUNT=false
|
||||
mountpoint="/mnt/$diskname"
|
||||
|
||||
if ! mountpoint -q "$mountpoint"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_test_write() {
|
||||
local testfile="$mountpoint/.writetest_$$"
|
||||
if : > "$testfile" 2> /dev/null; then
|
||||
rm -f "$testfile" 2> /dev/null || true
|
||||
return 0
|
||||
else
|
||||
rm -f "$testfile" 2> /dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local orig_mountoptions="$MOUNTOPTIONS"
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
|
||||
echo "... retrying mount with noserverino"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noserverino"
|
||||
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
|
||||
echo "... retrying mount with noperm"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noperm"
|
||||
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if mountpoint -q "$mountpoint"; then
|
||||
MOUNTED="readonly"
|
||||
else
|
||||
MOUNTED=false
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
mount_drive() {
|
||||
MOUNTED=true
|
||||
MOUNTOPTIONS="$1"
|
||||
MOUNTED=true
|
||||
MOUNTOPTIONS="$1"
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
|
||||
elif [[ "$FSTYPE" == "nfs" ]]; then
|
||||
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
|
||||
fi
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
|
||||
elif [[ "$FSTYPE" == "nfs" ]]; then
|
||||
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
|
||||
fi
|
||||
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
test_mount
|
||||
fi
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
test_mount
|
||||
fi
|
||||
}
|
||||
|
||||
# Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
|
||||
[[ "${MOUNTED:-false}" == "false" ]] || return 0
|
||||
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
|
||||
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
|
||||
[[ "${MOUNTED:-false}" == "false" ]] || return 0
|
||||
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
|
||||
|
||||
local err mountpoint
|
||||
mountpoint="/mnt/$diskname"
|
||||
err="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
|
||||
local err mountpoint
|
||||
mountpoint="/mnt/$diskname"
|
||||
err="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
|
||||
|
||||
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
|
||||
return 0
|
||||
fi
|
||||
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
|
||||
return 0
|
||||
fi
|
||||
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
CIFS_LADDER_ATTEMPTED=true
|
||||
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
|
||||
CIFS_LADDER_ATTEMPTED=true
|
||||
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
|
||||
|
||||
local base_opts try_opts vers vopt sectry
|
||||
local base_opts try_opts vers vopt sectry
|
||||
|
||||
base_opts="$MOUNTOPTIONS"
|
||||
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
||||
|
||||
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
|
||||
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
|
||||
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
|
||||
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
|
||||
base_opts="$MOUNTOPTIONS"
|
||||
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
||||
base_opts="${base_opts//,mfsymlinks/}"
|
||||
base_opts="${base_opts//,nobrl/}"
|
||||
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
|
||||
|
||||
local -a vers_variants2=("2.1" "2.0" "1.0")
|
||||
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
|
||||
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
|
||||
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
|
||||
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
for vers in "${vers_variants[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
|
||||
base_opts="$MOUNTOPTIONS"
|
||||
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
||||
base_opts="${base_opts//,mfsymlinks/}"
|
||||
base_opts="${base_opts//,nobrl/}"
|
||||
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
|
||||
|
||||
return 0
|
||||
local -a vers_variants2=("2.1" "2.0" "1.0")
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########################
|
||||
@@ -185,289 +185,289 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
|
||||
if bashio::config.has_value 'networkdisks'; then
|
||||
|
||||
echo "Mounting network share(s)..."
|
||||
echo "Mounting network share(s)..."
|
||||
|
||||
MOREDISKS="$(bashio::config 'networkdisks')"
|
||||
USERNAME="$(bashio::config 'cifsusername')"
|
||||
PASSWORD="$(bashio::config 'cifspassword')"
|
||||
MOREDISKS="$(bashio::config 'networkdisks')"
|
||||
USERNAME="$(bashio::config 'cifsusername')"
|
||||
PASSWORD="$(bashio::config 'cifspassword')"
|
||||
|
||||
SMBVERS=""
|
||||
SECVERS=""
|
||||
CHARSET=",iocharset=utf8"
|
||||
|
||||
MOREDISKS="${MOREDISKS//$'\r'/}"
|
||||
MOREDISKS="${MOREDISKS//$'\n'/,}"
|
||||
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
|
||||
DOMAINCLIENT=""
|
||||
CIFSDOMAIN=""
|
||||
if bashio::config.has_value 'cifsdomain'; then
|
||||
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
|
||||
echo "... using domain $CIFSDOMAIN"
|
||||
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
|
||||
fi
|
||||
|
||||
PUID=",uid=$(id -u)"
|
||||
PGID=",gid=$(id -g)"
|
||||
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
|
||||
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
|
||||
PUID=",uid=$(bashio::config 'PUID')"
|
||||
PGID=",gid=$(bashio::config 'PGID')"
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
|
||||
|
||||
for disk in "${DISK_LIST[@]}"; do
|
||||
CRED_FILE=""
|
||||
cleanup_cred
|
||||
|
||||
disk="${disk//$'\r'/}"
|
||||
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
[[ -z "$disk" ]] && continue
|
||||
|
||||
disk="$(echo "$disk" | sed 's,/$,,')"
|
||||
disk="${disk//"\040"/ }"
|
||||
|
||||
FSTYPE="cifs"
|
||||
if [[ "$disk" =~ ^nfs:// ]]; then
|
||||
FSTYPE="nfs"
|
||||
disk="${disk#nfs://}"
|
||||
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||
FSTYPE="nfs"
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
|
||||
else
|
||||
server="${disk%%:*}"
|
||||
fi
|
||||
|
||||
diskname="$disk"
|
||||
diskname="${diskname//\\//}"
|
||||
diskname="${diskname##*/}"
|
||||
|
||||
if mountpoint -q "/mnt/$diskname" 2>/dev/null; then
|
||||
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
|
||||
continue
|
||||
fi
|
||||
|
||||
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
|
||||
: >"$ERRORCODE_FILE" || true
|
||||
|
||||
MOUNTED=false
|
||||
CIFS_LADDER_ATTEMPTED=false
|
||||
SMBVERS_FORCE=""
|
||||
SECVERS_FORCE=""
|
||||
SMBVERS=""
|
||||
SECVERS=""
|
||||
CHARSET=",iocharset=utf8"
|
||||
|
||||
echo "... mounting ($FSTYPE) $disk"
|
||||
MOREDISKS="${MOREDISKS//$'\r'/}"
|
||||
MOREDISKS="${MOREDISKS//$'\n'/,}"
|
||||
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
|
||||
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
else
|
||||
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
|
||||
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
DOMAINCLIENT=""
|
||||
CIFSDOMAIN=""
|
||||
if bashio::config.has_value 'cifsdomain'; then
|
||||
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
|
||||
echo "... using domain $CIFSDOMAIN"
|
||||
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
|
||||
fi
|
||||
|
||||
mkdir -p "/mnt/$diskname"
|
||||
chown root:root "/mnt/$diskname"
|
||||
PUID=",uid=$(id -u)"
|
||||
PGID=",gid=$(id -g)"
|
||||
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
|
||||
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
|
||||
PUID=",uid=$(bashio::config 'PUID')"
|
||||
PGID=",gid=$(bashio::config 'PGID')"
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
|
||||
chmod 600 "$CRED_FILE"
|
||||
{
|
||||
printf 'username=%s\n' "$USERNAME"
|
||||
printf 'password=%s\n' "$PASSWORD"
|
||||
if [[ -n "${CIFSDOMAIN:-}" ]]; then
|
||||
printf 'domain=%s\n' "$CIFSDOMAIN"
|
||||
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
|
||||
|
||||
for disk in "${DISK_LIST[@]}"; do
|
||||
CRED_FILE=""
|
||||
cleanup_cred
|
||||
|
||||
disk="${disk//$'\r'/}"
|
||||
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
[[ -z "$disk" ]] && continue
|
||||
|
||||
disk="$(echo "$disk" | sed 's,/$,,')"
|
||||
disk="${disk//"\040"/ }"
|
||||
|
||||
FSTYPE="cifs"
|
||||
if [[ "$disk" =~ ^nfs:// ]]; then
|
||||
FSTYPE="nfs"
|
||||
disk="${disk#nfs://}"
|
||||
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||
FSTYPE="nfs"
|
||||
fi
|
||||
} >"$CRED_FILE"
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
fi
|
||||
else
|
||||
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
|
||||
fi
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
|
||||
else
|
||||
server="${disk%%:*}"
|
||||
fi
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||
if ! echo "$output" | grep -q "445/open"; then
|
||||
if echo "$output" | grep -q "/open"; then
|
||||
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
|
||||
else
|
||||
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
|
||||
fi
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
diskname="$disk"
|
||||
diskname="${diskname//\\//}"
|
||||
diskname="${diskname##*/}"
|
||||
|
||||
if mountpoint -q "/mnt/$diskname" 2> /dev/null; then
|
||||
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
|
||||
continue
|
||||
fi
|
||||
echo "...... $server is confirmed reachable"
|
||||
else
|
||||
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
|
||||
fi
|
||||
|
||||
if command -v smbclient >/dev/null 2>&1; then
|
||||
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
|
||||
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
bashio::addon.stop
|
||||
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
|
||||
echo "... using SMBv1"
|
||||
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
|
||||
SMBVERS_FORCE=",vers=1.0"
|
||||
SECVERS_FORCE=",sec=ntlm"
|
||||
elif ! echo "$OUTPUT" | grep -q "Disk"; then
|
||||
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
|
||||
else
|
||||
echo "...... credentials are valid"
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
|
||||
fi
|
||||
|
||||
SMBRAW=""
|
||||
SMB1_DETECTED=false
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
|
||||
SMBRAW="$(
|
||||
echo "$NMAP_OUTPUT" \
|
||||
| awk '/SMB2_DIALECT_/ {print $NF}' \
|
||||
| sed 's/SMB2_DIALECT_//' \
|
||||
| tr -d '_' \
|
||||
| sort -V | tail -n 1 || true
|
||||
)"
|
||||
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
|
||||
SMB1_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
|
||||
: > "$ERRORCODE_FILE" || true
|
||||
|
||||
MOUNTED=false
|
||||
CIFS_LADDER_ATTEMPTED=false
|
||||
SMBVERS_FORCE=""
|
||||
SECVERS_FORCE=""
|
||||
SMBVERS=""
|
||||
case "$SMBRAW" in
|
||||
311) SMBVERS=",vers=3.1.1" ;;
|
||||
302) SMBVERS=",vers=3.02" ;;
|
||||
300) SMBVERS=",vers=3.0" ;;
|
||||
210) SMBVERS=",vers=2.1" ;;
|
||||
202|200) SMBVERS=",vers=2.0" ;;
|
||||
*) SMBVERS="" ;;
|
||||
esac
|
||||
SECVERS=""
|
||||
|
||||
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
|
||||
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
|
||||
SMBVERS=",vers=1.0"
|
||||
SECVERS=",sec=ntlm"
|
||||
fi
|
||||
echo "... mounting ($FSTYPE) $disk"
|
||||
|
||||
if [[ -n "$SMBVERS" ]]; then
|
||||
echo "...... SMB version detected : ${SMBVERS#,vers=}"
|
||||
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &>/dev/null; then
|
||||
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
|
||||
SECVERS=",sec=ntlm"
|
||||
SMBVERS=",vers=1.0"
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
|
||||
echo "Invalid CIFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
else
|
||||
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
|
||||
SMBVERS=",vers=3.1.1"
|
||||
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
|
||||
echo "Invalid NFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SMBVERS_FORCE" ]]; then
|
||||
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
|
||||
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
|
||||
fi
|
||||
SMBVERS="$SMBVERS_FORCE"
|
||||
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
|
||||
mkdir -p "/mnt/$diskname"
|
||||
chown root:root "/mnt/$diskname"
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
|
||||
chmod 600 "$CRED_FILE"
|
||||
{
|
||||
printf 'username=%s\n' "$USERNAME"
|
||||
printf 'password=%s\n' "$PASSWORD"
|
||||
if [[ -n "${CIFSDOMAIN:-}" ]]; then
|
||||
printf 'domain=%s\n' "$CIFSDOMAIN"
|
||||
fi
|
||||
} > "$CRED_FILE"
|
||||
fi
|
||||
|
||||
SECVERS_BASE="$SECVERS"
|
||||
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
|
||||
fi
|
||||
done
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
fi
|
||||
else
|
||||
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
|
||||
fi
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
||||
if ! echo "$output" | grep -q "445/open"; then
|
||||
if echo "$output" | grep -q "/open"; then
|
||||
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
|
||||
else
|
||||
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
|
||||
fi
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
continue
|
||||
fi
|
||||
echo "...... $server is confirmed reachable"
|
||||
else
|
||||
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
|
||||
fi
|
||||
|
||||
if command -v smbclient > /dev/null 2>&1; then
|
||||
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
|
||||
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
bashio::addon.stop
|
||||
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
|
||||
echo "... using SMBv1"
|
||||
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
|
||||
SMBVERS_FORCE=",vers=1.0"
|
||||
SECVERS_FORCE=",sec=ntlm"
|
||||
elif ! echo "$OUTPUT" | grep -q "Disk"; then
|
||||
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
|
||||
else
|
||||
echo "...... credentials are valid"
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
|
||||
fi
|
||||
|
||||
SMBRAW=""
|
||||
SMB1_DETECTED=false
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)"
|
||||
SMBRAW="$(
|
||||
echo "$NMAP_OUTPUT" \
|
||||
| awk '/SMB2_DIALECT_/ {print $NF}' \
|
||||
| sed 's/SMB2_DIALECT_//' \
|
||||
| tr -d '_' \
|
||||
| sort -V | tail -n 1 || true
|
||||
)"
|
||||
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
|
||||
SMB1_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
|
||||
SMBVERS=""
|
||||
case "$SMBRAW" in
|
||||
311) SMBVERS=",vers=3.1.1" ;;
|
||||
302) SMBVERS=",vers=3.02" ;;
|
||||
300) SMBVERS=",vers=3.0" ;;
|
||||
210) SMBVERS=",vers=2.1" ;;
|
||||
202 | 200) SMBVERS=",vers=2.0" ;;
|
||||
*) SMBVERS="" ;;
|
||||
esac
|
||||
|
||||
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
|
||||
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
|
||||
SMBVERS=",vers=1.0"
|
||||
SECVERS=",sec=ntlm"
|
||||
fi
|
||||
|
||||
if [[ -n "$SMBVERS" ]]; then
|
||||
echo "...... SMB version detected : ${SMBVERS#,vers=}"
|
||||
elif command -v smbclient > /dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &> /dev/null; then
|
||||
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
|
||||
SECVERS=",sec=ntlm"
|
||||
SMBVERS=",vers=1.0"
|
||||
else
|
||||
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
|
||||
SMBVERS=",vers=3.1.1"
|
||||
fi
|
||||
|
||||
if [[ -n "$SMBVERS_FORCE" ]]; then
|
||||
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
|
||||
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
|
||||
fi
|
||||
SMBVERS="$SMBVERS_FORCE"
|
||||
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
|
||||
fi
|
||||
|
||||
SECVERS_BASE="$SECVERS"
|
||||
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
fi
|
||||
|
||||
else
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
||||
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
|
||||
bashio::log.fatal "...... $server is reachable but NFS ports not open"
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
|
||||
fi
|
||||
|
||||
for NFVER in 4.2 4.1 4 3; do
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
|
||||
bashio::log.fatal "...... $server is reachable but NFS ports not open"
|
||||
fi
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
|
||||
bashio::log.warning ""
|
||||
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
|
||||
bashio::log.warning ""
|
||||
fi
|
||||
|
||||
cleanup_cred
|
||||
|
||||
elif [[ "$MOUNTED" == "readonly" ]]; then
|
||||
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
|
||||
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
cleanup_cred
|
||||
|
||||
else
|
||||
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
|
||||
bashio::log.fatal "Here is some debugging info :"
|
||||
if command -v smbclient > /dev/null 2>&1; then
|
||||
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
|
||||
else
|
||||
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
|
||||
fi
|
||||
|
||||
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
fi
|
||||
else
|
||||
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
|
||||
mount_drive "rw"
|
||||
fi
|
||||
|
||||
ERR_READ="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
|
||||
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
|
||||
|
||||
umount "/mnt/$diskname" 2> /dev/null || true
|
||||
rmdir "/mnt/$diskname" 2> /dev/null || true
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
|
||||
bashio::addon.stop
|
||||
fi
|
||||
|
||||
for NFVER in 4.2 4.1 4 3; do
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
|
||||
bashio::log.warning ""
|
||||
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
|
||||
bashio::log.warning ""
|
||||
fi
|
||||
|
||||
cleanup_cred
|
||||
|
||||
elif [[ "$MOUNTED" == "readonly" ]]; then
|
||||
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
|
||||
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
cleanup_cred
|
||||
|
||||
else
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
|
||||
bashio::log.fatal "Here is some debugging info :"
|
||||
if command -v smbclient >/dev/null 2>&1; then
|
||||
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
|
||||
else
|
||||
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
|
||||
fi
|
||||
|
||||
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||
fi
|
||||
else
|
||||
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
|
||||
mount_drive "rw"
|
||||
fi
|
||||
|
||||
ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
|
||||
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
|
||||
|
||||
umount "/mnt/$diskname" 2>/dev/null || true
|
||||
rmdir "/mnt/$diskname" 2>/dev/null || true
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
|
||||
bashio::addon.stop
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -138,9 +138,9 @@ append_unique_line() {
|
||||
# $1=file, $2=line
|
||||
local _file="$1"
|
||||
local _line="$2"
|
||||
mkdir -p "$(dirname "$_file")" 2>/dev/null || true
|
||||
touch "$_file" 2>/dev/null || true
|
||||
grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file"
|
||||
mkdir -p "$(dirname "$_file")" 2> /dev/null || true
|
||||
touch "$_file" 2> /dev/null || true
|
||||
grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file"
|
||||
}
|
||||
|
||||
while IFS= read -r line; do
|
||||
|
||||
@@ -7,27 +7,33 @@ set -e
|
||||
##########################################
|
||||
|
||||
pick_exec_dir() {
|
||||
local d
|
||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
if "$t" >/dev/null 2>&1; then
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
local d
|
||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
chmod 700 "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
if "$t" > /dev/null 2>&1; then
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
EXEC_DIR="$(pick_exec_dir || true)"
|
||||
if [ -z "${EXEC_DIR:-}" ]; then
|
||||
echo "ERROR: Could not find an exec-capable writable directory."
|
||||
exit 1
|
||||
echo "ERROR: Could not find an exec-capable writable directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
######################
|
||||
@@ -35,14 +41,14 @@ fi
|
||||
######################
|
||||
|
||||
candidate_shebangs=(
|
||||
"/command/with-contenv bashio"
|
||||
"/usr/bin/with-contenv bashio"
|
||||
"/usr/bin/env bashio"
|
||||
"/usr/bin/bashio"
|
||||
"/usr/bin/bash"
|
||||
"/bin/bash"
|
||||
"/usr/bin/sh"
|
||||
"/bin/sh"
|
||||
"/command/with-contenv bashio"
|
||||
"/usr/bin/with-contenv bashio"
|
||||
"/usr/bin/env bashio"
|
||||
"/usr/bin/bashio"
|
||||
"/usr/bin/bash"
|
||||
"/bin/bash"
|
||||
"/usr/bin/sh"
|
||||
"/bin/sh"
|
||||
)
|
||||
|
||||
SHEBANG_ERRORS=()
|
||||
@@ -85,76 +91,76 @@ echo "${_bv:-PROBE_OK}"
|
||||
'
|
||||
|
||||
validate_shebang() {
|
||||
local candidate="$1"
|
||||
local tmp out rc
|
||||
local errfile msg
|
||||
local candidate="$1"
|
||||
local tmp out rc
|
||||
local errfile msg
|
||||
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=( $candidate )
|
||||
local exe="${cmd[0]}"
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=($candidate)
|
||||
local exe="${cmd[0]}"
|
||||
|
||||
if [ ! -x "$exe" ]; then
|
||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||
if [ ! -x "$exe" ]; then
|
||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||
return 1
|
||||
fi
|
||||
|
||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} > "$tmp"
|
||||
chmod 700 "$tmp" 2> /dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2> "$errfile")"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
rm -f "$tmp" 2> /dev/null || true
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||
if [ -s "$errfile" ]; then
|
||||
msg+=$' stderr:\n'
|
||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||
else
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} >"$tmp"
|
||||
chmod 700 "$tmp" 2>/dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2>"$errfile")"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
rm -f "$tmp" 2>/dev/null || true
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||
if [ -s "$errfile" ]; then
|
||||
msg+=$' stderr:\n'
|
||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||
else
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
shebang=""
|
||||
for candidate in "${candidate_shebangs[@]}"; do
|
||||
if validate_shebang "$candidate"; then
|
||||
shebang="$candidate"
|
||||
break
|
||||
fi
|
||||
if validate_shebang "$candidate"; then
|
||||
shebang="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$shebang" ]; then
|
||||
echo "ERROR: No valid shebang found." >&2
|
||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||
fi
|
||||
exit 1
|
||||
echo "ERROR: No valid shebang found." >&2
|
||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v bashio::addon.version >/dev/null 2>&1; then
|
||||
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! command -v bashio::addon.version > /dev/null 2>&1; then
|
||||
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
##################
|
||||
@@ -162,9 +168,9 @@ fi
|
||||
##################
|
||||
|
||||
# Exit if /config is not mounted or HA not used
|
||||
if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then
|
||||
echo "..."
|
||||
exit 0
|
||||
if [ ! -d /config ] || ! bashio::supervisor.ping 2> /dev/null; then
|
||||
echo "..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Define slug
|
||||
@@ -173,11 +179,11 @@ slug="${slug#*_}"
|
||||
|
||||
# Check type of config folder
|
||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
else
|
||||
CONFIGLOCATION="/config/addons_autoscripts"
|
||||
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
||||
CONFIGLOCATION="/config/addons_autoscripts"
|
||||
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
||||
fi
|
||||
|
||||
# Default location
|
||||
@@ -189,28 +195,28 @@ bashio::log.green "Execute $CONFIGFILEBROWSER if existing"
|
||||
|
||||
# Download template if no script found and exit
|
||||
if [ ! -f "$CONFIGSOURCE" ]; then
|
||||
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
|
||||
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
|
||||
exit 0
|
||||
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
|
||||
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert scripts to linux
|
||||
dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true
|
||||
dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true
|
||||
chmod +x "$CONFIGSOURCE"
|
||||
|
||||
if grep -q 'bashio' "$CONFIGSOURCE"; then
|
||||
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
|
||||
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
|
||||
else
|
||||
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
|
||||
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
|
||||
fi
|
||||
|
||||
# Check if there is actual commands
|
||||
while IFS= read -r line; do
|
||||
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
|
||||
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
|
||||
bashio::log.green "... script found, executing"
|
||||
"$CONFIGSOURCE"
|
||||
break
|
||||
fi
|
||||
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
|
||||
bashio::log.green "... script found, executing"
|
||||
"$CONFIGSOURCE"
|
||||
break
|
||||
fi
|
||||
done < "$CONFIGSOURCE"
|
||||
|
||||
@@ -9,12 +9,12 @@ set -e
|
||||
# Avoid usage of local dns such as adguard home or pihole\n"
|
||||
|
||||
if bashio::config.has_value 'DNS_server'; then
|
||||
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
|
||||
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
|
||||
# Define variables
|
||||
DNSSERVER=$(bashio::config 'DNS_server')
|
||||
DNS=""
|
||||
DNSLIST=""
|
||||
|
||||
|
||||
# Get DNS servers
|
||||
# shellcheck disable=SC2086
|
||||
for server in ${DNSSERVER//,/ }; do # Separate comma separated values
|
||||
@@ -26,7 +26,7 @@ if bashio::config.has_value 'DNS_server'; then
|
||||
bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# Only add DNS if there are DNS set
|
||||
# shellcheck disable=SC2236
|
||||
if [[ -n "${DNS:-}" ]]; then
|
||||
|
||||
@@ -33,293 +33,297 @@ _BASHIO_COLOR=1
|
||||
[ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0
|
||||
|
||||
_bashio_color() {
|
||||
[ "$_BASHIO_COLOR" = "1" ] || return 0
|
||||
case "${1:-}" in
|
||||
blue) printf '\033[34m' ;;
|
||||
green) printf '\033[32m' ;;
|
||||
yellow) printf '\033[33m' ;;
|
||||
red) printf '\033[31m' ;;
|
||||
magenta) printf '\033[35m' ;;
|
||||
reset) printf '\033[0m' ;;
|
||||
*) printf '' ;;
|
||||
esac
|
||||
[ "$_BASHIO_COLOR" = "1" ] || return 0
|
||||
case "${1:-}" in
|
||||
blue) printf '\033[34m' ;;
|
||||
green) printf '\033[32m' ;;
|
||||
yellow) printf '\033[33m' ;;
|
||||
red) printf '\033[31m' ;;
|
||||
magenta) printf '\033[35m' ;;
|
||||
reset) printf '\033[0m' ;;
|
||||
*) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_bashio_log() {
|
||||
local c="${1:-}"; shift || true
|
||||
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
|
||||
local c="${1:-}"
|
||||
shift || true
|
||||
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
_bashio_is_true() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case "${1:-}" in
|
||||
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ENV mapping helper:
|
||||
# tries variants + prefixes and prints the value if env var is defined (even empty),
|
||||
# returning 0 when found, 1 when not found.
|
||||
_bashio_env_get() {
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
|
||||
local norm norm_uc raw_uc
|
||||
norm="$(printf '%s' "$key" | tr '.-' '__')"
|
||||
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
|
||||
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
local norm norm_uc raw_uc
|
||||
norm="$(printf '%s' "$key" | tr '.-' '__')"
|
||||
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
|
||||
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
|
||||
local variants=(
|
||||
"$key"
|
||||
"$raw_uc"
|
||||
"$norm"
|
||||
"$norm_uc"
|
||||
)
|
||||
local variants=(
|
||||
"$key"
|
||||
"$raw_uc"
|
||||
"$norm"
|
||||
"$norm_uc"
|
||||
)
|
||||
|
||||
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
|
||||
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
|
||||
|
||||
local v p name
|
||||
for v in "${variants[@]}"; do
|
||||
for p in "${prefixes[@]}"; do
|
||||
name="${p}${v}"
|
||||
if [ -n "${!name+x}" ]; then
|
||||
printf '%s' "${!name}"
|
||||
return 0
|
||||
fi
|
||||
local v p name
|
||||
for v in "${variants[@]}"; do
|
||||
for p in "${prefixes[@]}"; do
|
||||
name="${p}${v}"
|
||||
if [ -n "${!name+x}" ]; then
|
||||
printf '%s' "${!name}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
return 1
|
||||
}
|
||||
|
||||
# env presence (even if empty) used by config.exists
|
||||
_bashio_env_has() {
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
_bashio_env_get "$key" >/dev/null 2>&1
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
_bashio_env_get "$key" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# JSON options source (jq required). Prints value or empty; returns 0 always.
|
||||
_bashio_json_get() {
|
||||
local key="${1:-}"
|
||||
local file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
local key="${1:-}"
|
||||
local file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
|
||||
[ -n "$key" ] || return 0
|
||||
[ -n "$file" ] || return 0
|
||||
[ -f "$file" ] || return 0
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[ -n "$key" ] || return 0
|
||||
[ -n "$file" ] || return 0
|
||||
[ -f "$file" ] || return 0
|
||||
command -v jq > /dev/null 2>&1 || return 0
|
||||
|
||||
# getpath(split(".")) supports nested access; missing => empty
|
||||
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2>/dev/null || true
|
||||
# getpath(split(".")) supports nested access; missing => empty
|
||||
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2> /dev/null || true
|
||||
}
|
||||
|
||||
# Net wait using /dev/tcp with a timeout
|
||||
_bashio_tcp_wait() {
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
[ -n "$host" ] && [ -n "$port" ] || return 1
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
[ -n "$host" ] && [ -n "$port" ] || return 1
|
||||
|
||||
local start now
|
||||
start="$(date +%s)"
|
||||
while :; do
|
||||
if exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
|
||||
exec 3>&- 3<&-
|
||||
return 0
|
||||
fi
|
||||
now="$(date +%s)"
|
||||
if [ $((now - start)) -ge "$to" ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
local start now
|
||||
start="$(date +%s)"
|
||||
while :; do
|
||||
if exec 3<> "/dev/tcp/${host}/${port}" 2> /dev/null; then
|
||||
exec 3>&- 3<&-
|
||||
return 0
|
||||
fi
|
||||
now="$(date +%s)"
|
||||
if [ $((now - start)) -ge "$to" ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# Prefer nc if present, fallback to /dev/tcp
|
||||
_bashio_tcp_wait_nc() {
|
||||
command -v nc >/dev/null 2>&1 || return 1
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
# BusyBox and OpenBSD nc differ; cover both styles
|
||||
nc -z -w "$to" "$host" "$port" 2>/dev/null || nc -z "$host" "$port" 2>/dev/null
|
||||
command -v nc > /dev/null 2>&1 || return 1
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
# BusyBox and OpenBSD nc differ; cover both styles
|
||||
nc -z -w "$to" "$host" "$port" 2> /dev/null || nc -z "$host" "$port" 2> /dev/null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging API
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::log.blue() { _bashio_log blue "$*"; }
|
||||
bashio::log.green() { _bashio_log green "$*"; }
|
||||
bashio::log.yellow() { _bashio_log yellow "$*"; }
|
||||
bashio::log.red() { _bashio_log red "$*"; }
|
||||
bashio::log.blue() { _bashio_log blue "$*"; }
|
||||
bashio::log.green() { _bashio_log green "$*"; }
|
||||
bashio::log.yellow() { _bashio_log yellow "$*"; }
|
||||
bashio::log.red() { _bashio_log red "$*"; }
|
||||
bashio::log.magenta() { _bashio_log magenta "$*"; }
|
||||
|
||||
# Common aliases
|
||||
bashio::log.info() { bashio::log.blue "$@"; }
|
||||
bashio::log.info() { bashio::log.blue "$@"; }
|
||||
bashio::log.warning() { bashio::log.yellow "$@"; }
|
||||
bashio::log.error() { bashio::log.red "$@"; }
|
||||
bashio::log.debug() { printf '%s\n' "$*"; }
|
||||
bashio::log.error() { bashio::log.red "$@"; }
|
||||
bashio::log.debug() { printf '%s\n' "$*"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Supervisor shim
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::supervisor.ping() {
|
||||
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
|
||||
return 1
|
||||
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Add-on metadata
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
|
||||
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
|
||||
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
|
||||
bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
||||
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
|
||||
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
|
||||
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
|
||||
bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
||||
|
||||
bashio::addon.update_available() {
|
||||
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
|
||||
printf '%s' "true"
|
||||
else
|
||||
printf '%s' "false"
|
||||
fi
|
||||
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
|
||||
printf '%s' "true"
|
||||
else
|
||||
printf '%s' "false"
|
||||
fi
|
||||
}
|
||||
|
||||
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
|
||||
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
|
||||
bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; }
|
||||
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
|
||||
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
|
||||
|
||||
# Ports:
|
||||
# - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number
|
||||
# - non-numeric "WEB_PORT" -> resolve as config/env key
|
||||
bashio::addon.port() {
|
||||
local arg="${1:-}"
|
||||
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
||||
local v=""
|
||||
v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)"
|
||||
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)"
|
||||
printf '%s' "${v:-$arg}"
|
||||
else
|
||||
printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)"
|
||||
fi
|
||||
local arg="${1:-}"
|
||||
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
||||
local v=""
|
||||
v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)"
|
||||
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)"
|
||||
printf '%s' "${v:-$arg}"
|
||||
else
|
||||
printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)"
|
||||
fi
|
||||
}
|
||||
|
||||
# addon.option : write/delete option in JSON when possible; fallback export env
|
||||
bashio::addon.option() {
|
||||
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
[ -n "$key" ] || return 0
|
||||
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
[ -n "$key" ] || return 0
|
||||
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if [ "$value" = "__BASHIO_UNSET__" ]; then
|
||||
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||
else
|
||||
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if [ "$value" = "__BASHIO_UNSET__" ]; then
|
||||
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
else
|
||||
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
|
||||
if [ "$value" != "__BASHIO_UNSET__" ]; then
|
||||
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
|
||||
fi
|
||||
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
|
||||
if [ "$value" != "__BASHIO_UNSET__" ]; then
|
||||
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# System info
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::info.operating_system() {
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
|
||||
else
|
||||
printf '%s' "Linux"
|
||||
fi
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
|
||||
else
|
||||
printf '%s' "Linux"
|
||||
fi
|
||||
}
|
||||
bashio::info.arch() { uname -m; }
|
||||
bashio::info.machine() { uname -m; }
|
||||
bashio::info.homeassistant(){ printf '%s' "standalone"; }
|
||||
bashio::info.supervisor() { printf '%s' "standalone"; }
|
||||
bashio::info.arch() { uname -m; }
|
||||
bashio::info.machine() { uname -m; }
|
||||
bashio::info.homeassistant() { printf '%s' "standalone"; }
|
||||
bashio::info.supervisor() { printf '%s' "standalone"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config API
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::config() {
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || {
|
||||
printf '%s' ""
|
||||
return 0
|
||||
}
|
||||
|
||||
local v=""
|
||||
if _bashio_env_get "$key" >/dev/null 2>&1; then
|
||||
v="$(_bashio_env_get "$key" 2>/dev/null || true)"
|
||||
fi
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
|
||||
printf '%s' "${v:-}"
|
||||
local v=""
|
||||
if _bashio_env_get "$key" > /dev/null 2>&1; then
|
||||
v="$(_bashio_env_get "$key" 2> /dev/null || true)"
|
||||
fi
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
|
||||
printf '%s' "${v:-}"
|
||||
}
|
||||
|
||||
bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; }
|
||||
|
||||
bashio::config.true() {
|
||||
_bashio_is_true "$(bashio::config "$1")"
|
||||
_bashio_is_true "$(bashio::config "$1")"
|
||||
}
|
||||
|
||||
# config.exists : key is present (env or JSON), even if value is empty
|
||||
bashio::config.exists() {
|
||||
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
[ -n "$key" ] || return 1
|
||||
|
||||
if _bashio_env_has "$key"; then
|
||||
return 0
|
||||
fi
|
||||
if _bashio_env_has "$key"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
||||
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" > /dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
return 1
|
||||
}
|
||||
|
||||
# Common "require.*" shims (advisory/no-op in standalone)
|
||||
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
|
||||
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
|
||||
bashio::config.require.username() { :; }
|
||||
bashio::config.require.password() { :; }
|
||||
bashio::config.require.port() { :; }
|
||||
bashio::config.require.port() { :; }
|
||||
|
||||
# config.array:
|
||||
# Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"].
|
||||
# Prints one item per line.
|
||||
bashio::config.array() {
|
||||
local key="${1:-}" raw
|
||||
raw="$(bashio::config "$key")"
|
||||
[ -n "$raw" ] || return 0
|
||||
local key="${1:-}" raw
|
||||
raw="$(bashio::config "$key")"
|
||||
[ -n "$raw" ] || return 0
|
||||
|
||||
if command -v jq >/dev/null 2>&1 && printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
|
||||
printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0
|
||||
fi
|
||||
if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then
|
||||
printf '%s' "$raw" | jq -r '.[]' 2> /dev/null && return 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$raw" | grep -q ','; then
|
||||
printf '%s' "$raw" | tr ',' '\n'
|
||||
return 0
|
||||
fi
|
||||
if printf '%s' "$raw" | grep -q ','; then
|
||||
printf '%s' "$raw" | tr ',' '\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$raw"
|
||||
printf '%s\n' "$raw"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# var helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::var.true() { _bashio_is_true "${1:-}"; }
|
||||
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
|
||||
bashio::var.true() { _bashio_is_true "${1:-}"; }
|
||||
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
|
||||
bashio::var.has_value() { [ -n "${1:-}" ]; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Filesystem helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
|
||||
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
|
||||
bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
|
||||
bashio::fs.file_contains() {
|
||||
local f="${1:-}" p="${2:-}"
|
||||
[ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null
|
||||
local f="${1:-}" p="${2:-}"
|
||||
[ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -327,25 +331,25 @@ bashio::fs.file_contains() {
|
||||
# -----------------------------------------------------------------------------
|
||||
# Wait for TCP service: bashio::net.wait_for host port [timeout]
|
||||
bashio::net.wait_for() {
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
|
||||
_bashio_tcp_wait "$host" "$port" "$to"
|
||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
|
||||
_bashio_tcp_wait "$host" "$port" "$to"
|
||||
}
|
||||
|
||||
# DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty)
|
||||
bashio::dns.host() {
|
||||
local h="${1:-}"
|
||||
[ -n "$h" ] || return 1
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
getent ahostsv4 "$h" | awk '{print $1; exit}'
|
||||
else
|
||||
nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}'
|
||||
fi
|
||||
local h="${1:-}"
|
||||
[ -n "$h" ] || return 1
|
||||
if command -v getent > /dev/null 2>&1; then
|
||||
getent ahostsv4 "$h" | awk '{print $1; exit}'
|
||||
else
|
||||
nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Hostname
|
||||
bashio::host.hostname() {
|
||||
command -v hostname >/dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
||||
command -v hostname > /dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -355,40 +359,43 @@ bashio::host.hostname() {
|
||||
# bashio::services "mqtt" "host"
|
||||
# bashio::services.available "mqtt"
|
||||
bashio::services() {
|
||||
local svc="${1:-}" key="${2:-}"
|
||||
[ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||
local svc="${1:-}" key="${2:-}"
|
||||
[ -n "$svc" ] && [ -n "$key" ] || {
|
||||
printf '%s' ""
|
||||
return 0
|
||||
}
|
||||
|
||||
local upper svc_upper var v=""
|
||||
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
|
||||
local upper svc_upper var v=""
|
||||
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
|
||||
|
||||
# Common mappings
|
||||
case "$svc_upper:$upper" in
|
||||
MQTT:HOST) var="MQTT_HOST" ;;
|
||||
MQTT:PORT) var="MQTT_PORT" ;;
|
||||
MQTT:USERNAME) var="MQTT_USER" ;;
|
||||
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
|
||||
MQTT:TLS) var="MQTT_TLS" ;;
|
||||
MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;;
|
||||
MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;;
|
||||
MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;;
|
||||
MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
||||
MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;;
|
||||
*) var="${svc_upper}_${upper}" ;;
|
||||
esac
|
||||
# Common mappings
|
||||
case "$svc_upper:$upper" in
|
||||
MQTT:HOST) var="MQTT_HOST" ;;
|
||||
MQTT:PORT) var="MQTT_PORT" ;;
|
||||
MQTT:USERNAME) var="MQTT_USER" ;;
|
||||
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
|
||||
MQTT:TLS) var="MQTT_TLS" ;;
|
||||
MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;;
|
||||
MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;;
|
||||
MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;;
|
||||
MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
||||
MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;;
|
||||
*) var="${svc_upper}_${upper}" ;;
|
||||
esac
|
||||
|
||||
v="$(_bashio_env_get "$var" 2>/dev/null || true)"
|
||||
if [ -z "$v" ]; then
|
||||
v="$(_bashio_json_get "services.${svc}.${key}")"
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
|
||||
fi
|
||||
printf '%s' "${v:-}"
|
||||
v="$(_bashio_env_get "$var" 2> /dev/null || true)"
|
||||
if [ -z "$v" ]; then
|
||||
v="$(_bashio_json_get "services.${svc}.${key}")"
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
|
||||
fi
|
||||
printf '%s' "${v:-}"
|
||||
}
|
||||
|
||||
bashio::services.available() {
|
||||
local svc="${1:-}" host
|
||||
host="$(bashio::services "$svc" "host")"
|
||||
[ -n "$host" ]
|
||||
local svc="${1:-}" host
|
||||
host="$(bashio::services "$svc" "host")"
|
||||
[ -n "$host" ]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -396,39 +403,43 @@ bashio::services.available() {
|
||||
# -----------------------------------------------------------------------------
|
||||
mkdir -p "$BASHIO_CACHE_DIR"
|
||||
bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; }
|
||||
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# jq wrapper (some add-ons call bashio::jq)
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::jq() { command -v jq >/dev/null 2>&1 && jq "$@"; }
|
||||
bashio::jq() { command -v jq > /dev/null 2>&1 && jq "$@"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Home Assistant token
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::homeassistant.token() {
|
||||
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
|
||||
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq >/dev/null 2>&1; then
|
||||
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)"
|
||||
fi
|
||||
printf '%s' "${t:-}"
|
||||
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
|
||||
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq > /dev/null 2>&1; then
|
||||
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2> /dev/null || true)"
|
||||
fi
|
||||
printf '%s' "${t:-}"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exit helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::exit.ok() { exit 0; }
|
||||
bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; }
|
||||
bashio::exit.ok() { exit 0; }
|
||||
bashio::exit.nok() {
|
||||
local m="${1:-}"
|
||||
[ -n "$m" ] && bashio::log.red "$m"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Core config check shim
|
||||
# -----------------------------------------------------------------------------
|
||||
# Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable
|
||||
bashio::core.check() {
|
||||
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
|
||||
eval "$STANDALONE_CORE_CHECK_CMD"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
|
||||
eval "$STANDALONE_CORE_CHECK_CMD"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
|
||||
mirror="http://ports.ubuntu.com/ubuntu-ports"
|
||||
security_mirror="$mirror"
|
||||
fi
|
||||
cat > /etc/apt/sources.list <<EOF2
|
||||
cat > /etc/apt/sources.list << EOF2
|
||||
deb ${mirror} ${codename} main restricted universe multiverse
|
||||
deb ${mirror} ${codename}-updates main restricted universe multiverse
|
||||
deb ${security_mirror} ${codename}-security main restricted universe multiverse
|
||||
EOF2
|
||||
;;
|
||||
debian)
|
||||
cat > /etc/apt/sources.list <<EOF2
|
||||
cat > /etc/apt/sources.list << EOF2
|
||||
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware
|
||||
|
||||
@@ -184,7 +184,7 @@ done
|
||||
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
|
||||
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
|
||||
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
|
||||
ALPINE_VER=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
|
||||
ALPINE_VER=$(cat /etc/alpine-release 2> /dev/null | cut -d. -f1,2)
|
||||
if [ -n "$ALPINE_VER" ]; then
|
||||
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
|
||||
apk update > /dev/null
|
||||
@@ -228,7 +228,7 @@ for files in "/etc/services.d" "/etc/cont-init.d"; do
|
||||
if ! ls $files 1> /dev/null 2>&1; then continue; fi
|
||||
|
||||
# Bashio
|
||||
if grep -q -rnw "${files}" -e 'bashio' 2>/dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
||||
if grep -q -rnw "${files}" -e 'bashio' 2> /dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
||||
[ "$VERBOSE" = true ] && echo "install bashio"
|
||||
BASHIO_VERSION="v0.17.5"
|
||||
mkdir -p /tmp/bashio
|
||||
|
||||
@@ -7,61 +7,13 @@
|
||||
|
||||
PID1=false
|
||||
if [ "$$" -eq 1 ]; then
|
||||
PID1=true
|
||||
echo "Starting as entrypoint"
|
||||
if [ -d /command ]; then
|
||||
ln -sf /command/* /usr/bin/ 2>/dev/null || true
|
||||
fi
|
||||
PID1=true
|
||||
echo "Starting as entrypoint"
|
||||
if [ -d /command ]; then
|
||||
ln -sf /command/* /usr/bin/ 2> /dev/null || true
|
||||
fi
|
||||
else
|
||||
echo "Starting custom scripts"
|
||||
fi
|
||||
|
||||
##########################################
|
||||
# Install the stop handler #
|
||||
##########################################
|
||||
|
||||
# As namespace PID 1 -- which is what "init: false" makes this script -- the kernel
|
||||
# discards any signal whose handler is still SIG_DFL. Installed at the end of startup,
|
||||
# as it used to be, the handler missed every stop that arrived while the Supervisor
|
||||
# probe or the cont-init chain was still running: Home Assistant waited out the grace
|
||||
# period for a SIGKILL and reported the add-on as failed. Nothing here is interrupted
|
||||
# mid-write by the move -- only PID 1 is signalled, and bash runs a trap at a command
|
||||
# boundary, so whatever external command is in flight reaps first.
|
||||
terminate() {
|
||||
local local_pid
|
||||
# Best-effort, so errexit must not apply: this runs with `set -e` in force (validate_shebang
|
||||
# leaves it on), and under errexit the first failing command aborts the handler and exits with
|
||||
# its status, skipping the child-kill loop and the `exit 0` below. Every command in the body
|
||||
# here is already guarded, but add-ons patch this function at build time -- postgres_15 and
|
||||
# postgres_17 sed an unguarded `pg_ctl ... stop` in right after the echo -- and that command
|
||||
# fails whenever the stop arrives before the database is up.
|
||||
set +e
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Only when this script is PID 1. Under Docker's own init the entrypoint is an ordinary
|
||||
# child and the signal handling above belongs to that init, not to us.
|
||||
if $PID1; then
|
||||
trap terminate SIGTERM SIGINT
|
||||
echo "Starting custom scripts"
|
||||
fi
|
||||
|
||||
##########################################
|
||||
@@ -69,31 +21,37 @@ fi
|
||||
##########################################
|
||||
|
||||
pick_exec_dir() {
|
||||
# Prefer locations that are commonly exec-capable in containers
|
||||
# and writable. Avoid /tmp because it may be mounted noexec.
|
||||
local d
|
||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
# Create a tiny test executable to confirm "exec" works
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
if "$t" >/dev/null 2>&1; then
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
# Prefer locations that are commonly exec-capable in containers
|
||||
# and writable. Avoid /tmp because it may be mounted noexec.
|
||||
local d
|
||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
# Create a tiny test executable to confirm "exec" works
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
chmod 700 "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
if "$t" > /dev/null 2>&1; then
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
EXEC_DIR="$(pick_exec_dir || true)"
|
||||
if [ -z "${EXEC_DIR:-}" ]; then
|
||||
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
|
||||
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
|
||||
exit 1
|
||||
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
|
||||
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
######################
|
||||
@@ -101,14 +59,14 @@ fi
|
||||
######################
|
||||
|
||||
candidate_shebangs=(
|
||||
"/command/with-contenv bashio"
|
||||
"/usr/bin/with-contenv bashio"
|
||||
"/usr/bin/env bashio"
|
||||
"/usr/bin/bashio"
|
||||
"/usr/bin/bash"
|
||||
"/bin/bash"
|
||||
"/usr/bin/sh"
|
||||
"/bin/sh"
|
||||
"/command/with-contenv bashio"
|
||||
"/usr/bin/with-contenv bashio"
|
||||
"/usr/bin/env bashio"
|
||||
"/usr/bin/bashio"
|
||||
"/usr/bin/bash"
|
||||
"/bin/bash"
|
||||
"/usr/bin/sh"
|
||||
"/bin/sh"
|
||||
)
|
||||
|
||||
SHEBANG_ERRORS=()
|
||||
@@ -152,68 +110,68 @@ echo "${_bv:-PROBE_OK}"
|
||||
'
|
||||
|
||||
validate_shebang() {
|
||||
local candidate="$1"
|
||||
local tmp out rc
|
||||
local errfile msg
|
||||
local candidate="$1"
|
||||
local tmp out rc
|
||||
local errfile msg
|
||||
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=( $candidate )
|
||||
local exe="${cmd[0]}"
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=($candidate)
|
||||
local exe="${cmd[0]}"
|
||||
|
||||
if [ ! -x "$exe" ]; then
|
||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||
if [ ! -x "$exe" ]; then
|
||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||
return 1
|
||||
fi
|
||||
|
||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} > "$tmp"
|
||||
chmod 700 "$tmp" 2> /dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2> "$errfile")"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
rm -f "$tmp" 2> /dev/null || true
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||
if [ -s "$errfile" ]; then
|
||||
msg+=$' stderr:\n'
|
||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||
else
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} >"$tmp"
|
||||
chmod 700 "$tmp" 2>/dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2>"$errfile")"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
rm -f "$tmp" 2>/dev/null || true
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||
if [ -s "$errfile" ]; then
|
||||
msg+=$' stderr:\n'
|
||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||
else
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
shebang=""
|
||||
for candidate in "${candidate_shebangs[@]}"; do
|
||||
if validate_shebang "$candidate"; then
|
||||
shebang="$candidate"
|
||||
break
|
||||
fi
|
||||
if validate_shebang "$candidate"; then
|
||||
shebang="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$shebang" ]; then
|
||||
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
|
||||
echo "Tried:" >&2
|
||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||
echo "Probe failures:" >&2
|
||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||
fi
|
||||
exit 1
|
||||
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
|
||||
echo "Tried:" >&2
|
||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||
echo "Probe failures:" >&2
|
||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#####################################
|
||||
@@ -245,28 +203,28 @@ S6_CONTAINER_ENV="/run/s6/container_environment"
|
||||
# Only when this script is PID 1 -- under /init it is the stage-2 hook and stage 1 has already
|
||||
# written the directory -- and only where with-contenv exists to care.
|
||||
if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then
|
||||
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
|
||||
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
|
||||
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
|
||||
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
|
||||
# the directory either absent or complete, never mid-dump.
|
||||
#
|
||||
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
|
||||
# entries, and writing name by name would merge into them and leave variables PID 1 does not
|
||||
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
|
||||
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
|
||||
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
|
||||
# any add-on uses, and not one s6 would tolerate either.
|
||||
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" &&
|
||||
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
|
||||
echo "Populated $S6_CONTAINER_ENV for with-contenv"
|
||||
else
|
||||
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
|
||||
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
|
||||
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
|
||||
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2>/dev/null || true
|
||||
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
|
||||
fi
|
||||
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
|
||||
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
|
||||
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
|
||||
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
|
||||
# the directory either absent or complete, never mid-dump.
|
||||
#
|
||||
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
|
||||
# entries, and writing name by name would merge into them and leave variables PID 1 does not
|
||||
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
|
||||
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
|
||||
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
|
||||
# any add-on uses, and not one s6 would tolerate either.
|
||||
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" \
|
||||
&& s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
|
||||
echo "Populated $S6_CONTAINER_ENV for with-contenv"
|
||||
else
|
||||
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
|
||||
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
|
||||
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
|
||||
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2> /dev/null || true
|
||||
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
|
||||
fi
|
||||
fi
|
||||
|
||||
####################################
|
||||
@@ -276,21 +234,21 @@ fi
|
||||
BASHIO_LIB=""
|
||||
BASHIO_LIB_FULL=false
|
||||
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
BASHIO_LIB="$f"
|
||||
# The real library, which talks to the Supervisor. The standalone shim below only reads
|
||||
# environment variables, which matters to wait_for_supervisor().
|
||||
BASHIO_LIB_FULL=true
|
||||
break
|
||||
fi
|
||||
if [ -f "$f" ]; then
|
||||
BASHIO_LIB="$f"
|
||||
# The real library, which talks to the Supervisor. The standalone shim below only reads
|
||||
# environment variables, which matters to wait_for_supervisor().
|
||||
BASHIO_LIB_FULL=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$BASHIO_LIB" ]; then
|
||||
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
BASHIO_LIB="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
BASHIO_LIB="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
##############################
|
||||
@@ -316,56 +274,56 @@ fi
|
||||
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
|
||||
|
||||
wait_for_supervisor() {
|
||||
local max="${HA_SUPERVISOR_WAIT:-30}"
|
||||
local started deadline remaining attempt announced=0
|
||||
local max="${HA_SUPERVISOR_WAIT:-30}"
|
||||
local started deadline remaining attempt announced=0
|
||||
|
||||
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
|
||||
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
|
||||
# the probe and would burn the whole ceiling on every boot.
|
||||
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
|
||||
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
|
||||
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
|
||||
command -v timeout >/dev/null 2>&1 || return 0
|
||||
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
|
||||
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
|
||||
# spin the loop below forever.
|
||||
case "$max" in '' | *[!0-9]*) return 0 ;; esac
|
||||
max=$((10#$max))
|
||||
[ "$max" -gt 0 ] || return 0
|
||||
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
|
||||
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
|
||||
# the probe and would burn the whole ceiling on every boot.
|
||||
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
|
||||
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
|
||||
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
|
||||
command -v timeout > /dev/null 2>&1 || return 0
|
||||
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
|
||||
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
|
||||
# spin the loop below forever.
|
||||
case "$max" in '' | *[!0-9]*) return 0 ;; esac
|
||||
max=$((10#$max))
|
||||
[ "$max" -gt 0 ] || return 0
|
||||
|
||||
started=$SECONDS
|
||||
deadline=$((started + max))
|
||||
started=$SECONDS
|
||||
deadline=$((started + max))
|
||||
|
||||
while :; do
|
||||
remaining=$((deadline - SECONDS))
|
||||
if [ "$remaining" -le 0 ]; then
|
||||
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
|
||||
return 0
|
||||
fi
|
||||
while :; do
|
||||
remaining=$((deadline - SECONDS))
|
||||
if [ "$remaining" -le 0 ]; then
|
||||
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# No single attempt may outlive the ceiling it is bounded by.
|
||||
attempt=5
|
||||
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
|
||||
# No single attempt may outlive the ceiling it is bounded by.
|
||||
attempt=5
|
||||
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
|
||||
|
||||
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
|
||||
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
|
||||
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
|
||||
# logging is dropped because a failed attempt here is expected, not news.
|
||||
# shellcheck disable=SC2016
|
||||
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
|
||||
_ "$BASHIO_LIB" >/dev/null 2>&1; then
|
||||
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
|
||||
return 0
|
||||
fi
|
||||
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
|
||||
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
|
||||
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
|
||||
# logging is dropped because a failed attempt here is expected, not news.
|
||||
# shellcheck disable=SC2016
|
||||
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
|
||||
_ "$BASHIO_LIB" > /dev/null 2>&1; then
|
||||
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$announced" -eq 0 ]; then
|
||||
echo "Waiting for the Supervisor API to report this add-on's network details..."
|
||||
announced=1
|
||||
fi
|
||||
if [ "$announced" -eq 0 ]; then
|
||||
echo "Waiting for the Supervisor API to report this add-on's network details..."
|
||||
announced=1
|
||||
fi
|
||||
|
||||
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
|
||||
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
|
||||
done
|
||||
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
|
||||
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_supervisor
|
||||
@@ -375,97 +333,97 @@ wait_for_supervisor
|
||||
####################
|
||||
|
||||
run_one_script() {
|
||||
local script="$1"
|
||||
local script="$1"
|
||||
|
||||
echo "$script: executing"
|
||||
echo "$script: executing"
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown "$(id -u)":"$(id -g)" "$script" || true
|
||||
chmod a+x "$script" || true
|
||||
else
|
||||
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
|
||||
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
|
||||
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
|
||||
fi
|
||||
|
||||
sed -i "1s|^.*|#!$shebang|" "$script"
|
||||
chmod +x "$script"
|
||||
|
||||
if [ "${ha_entry_source:-null}" = "true" ]; then
|
||||
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
|
||||
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
|
||||
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
|
||||
# shellcheck disable=SC1090
|
||||
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
|
||||
else
|
||||
_run_rc=0
|
||||
"$script" || _run_rc=$?
|
||||
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
|
||||
_run_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2>/dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$script"
|
||||
) || _run_rc=$?
|
||||
if [ "$_run_rc" -ne 0 ]; then
|
||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||
fi
|
||||
elif [ "$_run_rc" -ne 0 ]; then
|
||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown "$(id -u)":"$(id -g)" "$script" || true
|
||||
chmod a+x "$script" || true
|
||||
else
|
||||
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
|
||||
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
|
||||
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -i '1a exit 0' "$script"
|
||||
sed -i "1s|^.*|#!$shebang|" "$script"
|
||||
chmod +x "$script"
|
||||
|
||||
if [ "${ha_entry_source:-null}" = "true" ]; then
|
||||
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
|
||||
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
|
||||
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
|
||||
# shellcheck disable=SC1090
|
||||
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
|
||||
else
|
||||
_run_rc=0
|
||||
"$script" || _run_rc=$?
|
||||
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
|
||||
_run_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2> /dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$script"
|
||||
) || _run_rc=$?
|
||||
if [ "$_run_rc" -ne 0 ]; then
|
||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||
fi
|
||||
elif [ "$_run_rc" -ne 0 ]; then
|
||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -i '1a exit 0' "$script"
|
||||
}
|
||||
|
||||
if [ -d /etc/cont-init.d ]; then
|
||||
for SCRIPTS in /etc/cont-init.d/*; do
|
||||
[ -e "$SCRIPTS" ] || continue
|
||||
run_one_script "$SCRIPTS"
|
||||
done
|
||||
for SCRIPTS in /etc/cont-init.d/*; do
|
||||
[ -e "$SCRIPTS" ] || continue
|
||||
run_one_script "$SCRIPTS"
|
||||
done
|
||||
fi
|
||||
|
||||
if $PID1; then
|
||||
shopt -s nullglob
|
||||
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
|
||||
[ -f "$runfile" ] || continue
|
||||
echo "Starting: $runfile"
|
||||
sed -i "1s|^.*|#!$shebang|" "$runfile"
|
||||
chmod +x "$runfile"
|
||||
(
|
||||
restart_count=0
|
||||
max_restarts=5
|
||||
while true; do
|
||||
_svc_rc=0
|
||||
"$runfile" || _svc_rc=$?
|
||||
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
|
||||
_svc_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2>/dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$runfile"
|
||||
) || _svc_rc=$?
|
||||
fi
|
||||
rc=$_svc_rc
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "$runfile exited cleanly (exit 0), not restarting."
|
||||
break
|
||||
fi
|
||||
restart_count=$((restart_count + 1))
|
||||
if [ "$restart_count" -ge "$max_restarts" ]; then
|
||||
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
|
||||
break
|
||||
fi
|
||||
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
|
||||
sleep 5
|
||||
done
|
||||
) &
|
||||
done
|
||||
shopt -u nullglob
|
||||
shopt -s nullglob
|
||||
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
|
||||
[ -f "$runfile" ] || continue
|
||||
echo "Starting: $runfile"
|
||||
sed -i "1s|^.*|#!$shebang|" "$runfile"
|
||||
chmod +x "$runfile"
|
||||
(
|
||||
restart_count=0
|
||||
max_restarts=5
|
||||
while true; do
|
||||
_svc_rc=0
|
||||
"$runfile" || _svc_rc=$?
|
||||
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
|
||||
_svc_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2> /dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$runfile"
|
||||
) || _svc_rc=$?
|
||||
fi
|
||||
rc=$_svc_rc
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "$runfile exited cleanly (exit 0), not restarting."
|
||||
break
|
||||
fi
|
||||
restart_count=$((restart_count + 1))
|
||||
if [ "$restart_count" -ge "$max_restarts" ]; then
|
||||
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
|
||||
break
|
||||
fi
|
||||
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
|
||||
sleep 5
|
||||
done
|
||||
) &
|
||||
done
|
||||
shopt -u nullglob
|
||||
fi
|
||||
|
||||
######################
|
||||
@@ -473,18 +431,43 @@ fi
|
||||
######################
|
||||
|
||||
if $PID1; then
|
||||
echo " "
|
||||
echo -e "\033[0;32mEverything started!\033[0m"
|
||||
echo " "
|
||||
echo -e "\033[0;32mEverything started!\033[0m"
|
||||
|
||||
while :; do
|
||||
sleep infinity &
|
||||
wait $!
|
||||
done
|
||||
terminate() {
|
||||
local local_pid
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep > /dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap terminate SIGTERM SIGINT
|
||||
while :; do
|
||||
sleep infinity &
|
||||
wait $!
|
||||
done
|
||||
else
|
||||
echo " "
|
||||
echo -e "\033[0;32mStarting the upstream container\033[0m"
|
||||
echo " "
|
||||
if [ -f /docker-mods ]; then
|
||||
exec /docker-mods
|
||||
fi
|
||||
echo " "
|
||||
echo -e "\033[0;32mStarting the upstream container\033[0m"
|
||||
echo " "
|
||||
if [ -f /docker-mods ]; then
|
||||
exec /docker-mods
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
|
||||
in `.github/scripts/`.
|
||||
|
||||
| Workflow | Model | Trigger | Role |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
||||
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
||||
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
||||
@@ -176,7 +176,7 @@ prompt-only, not machine-enforced.
|
||||
## Linting Rules
|
||||
|
||||
| Tool | Config | Key ignores |
|
||||
|------|--------|------------|
|
||||
| ------ | -------- | ------------ |
|
||||
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
|
||||
| ShellCheck | `.shellcheckrc` | SC2002 |
|
||||
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |
|
||||
|
||||
@@ -75,7 +75,7 @@ You can add the following tags in the file :
|
||||
|
||||
### Addon version numbering
|
||||
|
||||
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
|
||||
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is _older_), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
|
||||
|
||||
The addon version is therefore derived from the upstream tag:
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
|
||||
|
||||
## 2.8.0 (2026-09-05)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.7.0 (2026-08-29)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.5.1 (2026-08-22)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.4.0 (2026-08-13)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.1.0 (2026-08-08)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.0.3 (2026-08-01)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.0.2 (2026-08-01)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 2.0.0 (2026-07-22)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 1.76.52 (2026-07-11)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
|
||||
## 1.76.51 (2026-06-17)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
## 1.76.49 (2026-06-05)
|
||||
|
||||
@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
|
||||
## Configuration
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
|
||||
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
|
||||
|
||||
## 1.85.0 (2026-08-29)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
|
||||
## 1.84.0 (2026-08-15)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
|
||||
## 1.83.0 (2026-08-01)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
|
||||
## 1.82.1 (2026-07-16)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
|
||||
## 1.81.0 (2026-07-04)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
## 1.80.0 (2026-06-05)
|
||||
|
||||
@@ -67,7 +67,7 @@ Default credentials: `admin` / `password` (change after first login).
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -44,7 +44,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
0
bazarr/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
bazarr/rootfs/etc/services.d/nginx/run
Normal file → Executable file
@@ -17,7 +17,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Organize & Edit
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Merge PDF | Split PDF | Organize PDF |
|
||||
| Delete Pages | Extract Pages | Reverse Pages |
|
||||
| Rotate PDF | Rotate Custom | Crop PDF |
|
||||
@@ -28,7 +28,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Convert TO PDF
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Word to PDF | Excel to PDF | PowerPoint to PDF |
|
||||
| Image to PDF | JPG to PDF | PNG to PDF |
|
||||
| BMP to PDF | TIFF to PDF | WEBP to PDF |
|
||||
@@ -44,7 +44,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Convert FROM PDF
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| PDF to DOCX | PDF to Excel | PDF to JPG |
|
||||
| PDF to PNG | PDF to BMP | PDF to TIFF |
|
||||
| PDF to WEBP | PDF to SVG | PDF to Text |
|
||||
@@ -54,7 +54,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Security & Metadata
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Encrypt PDF | Decrypt PDF | Change Permissions |
|
||||
| Remove Restrictions | Sign PDF | Digital Sign PDF |
|
||||
| Validate Signature | Edit Metadata | View Metadata |
|
||||
@@ -64,7 +64,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Enhance & Process
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Compress PDF | OCR PDF | Deskew PDF |
|
||||
| Rasterize PDF | Linearize PDF | PDF to PDF/A |
|
||||
| Adjust Colors | Invert Colors | Text Color |
|
||||
@@ -76,7 +76,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Forms & More
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Form Creator | Form Filler | Table of Contents |
|
||||
| Bookmark | PDF Editor | Extract Images |
|
||||
| Extract Tables | Extract Attachments | Edit Attachments |
|
||||
|
||||
0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run
Normal file → Executable file
0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run
Normal file → Executable file
@@ -1,25 +1,3 @@
|
||||
## 20260909.7 (10-09-2026)
|
||||
- Correct the "first daily detection consensus" README section (review feedback on PR #3056): a merged-upstream setting stays in the build rather than disappearing, every attempt for a species is held back until one is accepted rather than only the first, and the "known to every active bird model" exemption is scoped per audio source, matching the implementation.
|
||||
## 20260909.6 (10-09-2026)
|
||||
- Document the fork-only "first daily detection consensus" setting (alexbelgium/birdnet-go#63): requires a second model to confirm each bird species' first detection of the day. Off by default; no behaviour change unless enabled.
|
||||
## 20260909.5 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.4 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.3 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.2 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260908.2 (08-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260908.1 (08-09-2026)
|
||||
- Synced with upstream birdnet-go (4 commits); re-merges the open fork PRs, adding fork PR #62 (reanalyze a clip with every loaded model + one-click correction)
|
||||
## 20260908 (08-09-2026)
|
||||
- Synced with upstream birdnet-go (7 commits); re-merges the open fork PRs
|
||||
## 20260907 (07-09-2026)
|
||||
- Rebuild: re-merges the open fork PRs, picking up the updated fork PR #57
|
||||
## 20260901.4 (01-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260901.3 (01-09-2026)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home assistant add-on: Birdnet-Go (from source)
|
||||
|
||||
> **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on, except the fork-only settings listed under [Fork-only settings](#fork-only-settings).
|
||||
> **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on.
|
||||
|
||||
|
||||
|
||||
@@ -65,38 +65,6 @@ Additional variables can be configured using the config.yaml file found in /conf
|
||||
- Config_env.yaml
|
||||
Additional environment variables can be configured there
|
||||
|
||||
### Fork-only settings
|
||||
|
||||
These are currently fork-only settings from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork and are **not** in the standard add-on. [`merge-prs.sh`](./merge-prs.sh) syncs the fork's `main` with upstream before applying open PRs, so once a PR merges upstream the setting stays in this build — it just arrives via that sync instead of the PR-merge step, and stops being fork-only. Only a PR that is **closed without merging** drops its setting from later builds.
|
||||
|
||||
#### First daily detection consensus
|
||||
|
||||
Requires a second model to confirm the **first** detection of each bird species each day. Until one is accepted, every attempt for that species is held to the same two-model bar; only once a detection clears it does every later detection that day behave exactly as it does today, on a single model.
|
||||
|
||||
The first detection of a species in a day is the weakest evidence the pipeline produces, and it is the one that creates a "new species today" entry. Asking two models to agree on just that one detection removes most spurious new-species entries without slowing anything else down.
|
||||
|
||||
**Off by default.** Turn it on in the web UI under *Settings → Filters → First Daily Detection Consensus*, or in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
realtime:
|
||||
firstdailyconsensus:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The setting is re-read on each detection cycle, so it takes effect without restarting the add-on.
|
||||
|
||||
It deliberately does **nothing** in these cases, all of which keep today's single-model behaviour:
|
||||
|
||||
- you run only one bird model (the default) — a second opinion does not exist, so the rule can never trigger
|
||||
- the species is not a bird — bats and the non-bird sound classes Perch reports (insects, amphibians, mammals, `power_tool`, and so on)
|
||||
- the species is not known to *every* active bird model analyzing that audio source — a species only one of them can name could never reach two confirmations. With several sources running different model combinations, this is decided per source, not add-on-wide
|
||||
- a dynamic threshold has actually lowered the bar for that species, meaning you asked for a more permissive gate
|
||||
- the taxonomy or the database cannot be consulted — it fails open and accepts the detection
|
||||
|
||||
In practice it only bites when a single audio source has two or more bird models (for example BirdNET plus Perch) analyzing it, on species all of them can identify. The trade is fewer false new-species entries, at the cost of occasionally delaying a genuine first sighting until a second model agrees.
|
||||
|
||||
Requires [alexbelgium/birdnet-go#63](https://github.com/alexbelgium/birdnet-go/pull/63).
|
||||
|
||||
### MQTT and MariaDB auto-configuration (opt-in)
|
||||
|
||||
If the Home Assistant **MQTT** addon is installed and running and you set `mqtt_auto_config: true` in the addon options, the addon writes the HA Mosquitto credentials directly into BirdNET-Go's `config.yaml` on every startup: `realtime.mqtt.enabled`, `broker`, `username`, and `password` are populated, and the topic defaults to `birdnet`. In addition, it enables BirdNET-Go's **native Home Assistant MQTT auto-discovery** (`realtime.mqtt.homeassistant.enabled`), so the detection sensors show up in Home Assistant automatically — **no manual MQTT sensor YAML required** (the hand-written sensors in [HAINTEGRATION.md](./HAINTEGRATION.md) remain available if you prefer to build your own). Messages are also retained (`realtime.mqtt.retain: true`) so sensor states survive Home Assistant restarts. When the option is `false` (the default), the addon still logs the broker details and reminds you about the option whenever Mosquitto is detected — nothing is written.
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "20260909.7"
|
||||
version: "20260901.4"
|
||||
video: true
|
||||
|
||||
@@ -11,15 +11,6 @@
|
||||
# stamping) is written to the directory given as $1 so the Docker build can
|
||||
# compile it.
|
||||
#
|
||||
# With --check the script does not build anything: it performs the exact same
|
||||
# merge sequence, skips (instead of failing on) every conflicting PR, and prints
|
||||
# one "!!! CONFLICT pr=#N conflicts-with=... files=... " line per offender before
|
||||
# exiting 2. Use it to find conflicts *before* a build burns on them. This is a
|
||||
# different question from GitHub's `mergeable` field, which compares a PR against
|
||||
# its own base ref - for a stacked PR that base is another feature branch (often
|
||||
# stale, sometimes belonging to a closed PR), so GitHub can report CLEAN for a PR
|
||||
# that does not merge onto main at all.
|
||||
#
|
||||
# Environment:
|
||||
# BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go)
|
||||
# BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go)
|
||||
@@ -28,26 +19,7 @@
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
CHECK_ONLY="${MERGE_PRS_CHECK:-0}"
|
||||
TARGET_DIR=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--check) CHECK_ONLY=1 ;;
|
||||
-*) echo "unknown option: $1" >&2; exit 64 ;;
|
||||
*)
|
||||
# Last-one-wins would silently clone into the wrong directory if a caller ever
|
||||
# appended an argument; the pre-flag script used "${1}", so refuse rather than
|
||||
# quietly change which operand counts.
|
||||
if [ -n "${TARGET_DIR}" ]; then
|
||||
echo "usage: merge-prs.sh [--check] <target-dir>" >&2
|
||||
exit 64
|
||||
fi
|
||||
TARGET_DIR="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
: "${TARGET_DIR:?usage: merge-prs.sh [--check] <target-dir>}"
|
||||
TARGET_DIR="${1:?usage: merge-prs.sh <target-dir>}"
|
||||
|
||||
FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}"
|
||||
UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}"
|
||||
@@ -61,52 +33,6 @@ GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
|
||||
log() { echo ">>> $*"; }
|
||||
|
||||
# Conflicting PRs collected in --check mode: "number|scope|files|title".
|
||||
conflicting=()
|
||||
|
||||
LOCKFILE="frontend/package-lock.json"
|
||||
|
||||
# The single place that decides whether a conflicted merge is still acceptable.
|
||||
# package-lock.json is generated content and stacked PRs can carry an older copy even when
|
||||
# their source changes merge cleanly, so keep the lockfile already assembled on the base side
|
||||
# — but only when it is the sole conflict. Any source conflict stays fatal.
|
||||
# Returns 0 when it resolved and committed such a merge, 1 when the conflict is real.
|
||||
# BOTH the real merge and the --check probe must go through here: when only the real merge
|
||||
# applied the policy, the probe called a PR "conflicts-with=main" that the build would have
|
||||
# merged fine, and printed the opposite remediation to the true one.
|
||||
resolve_sole_lockfile() {
|
||||
local dir="$1"
|
||||
local -a conflicted
|
||||
mapfile -t conflicted < <(git -C "${dir}" diff --name-only --diff-filter=U)
|
||||
if [ "${#conflicted[@]}" -ne 1 ] || [ "${conflicted[0]}" != "${LOCKFILE}" ]; then
|
||||
return 1
|
||||
fi
|
||||
log "Resolving generated ${LOCKFILE} conflict using the base tree"
|
||||
git -C "${dir}" checkout --ours -- "${LOCKFILE}" || return 1
|
||||
git -C "${dir}" add "${LOCKFILE}" || return 1
|
||||
git -C "${dir}" commit --no-edit > /dev/null || return 1
|
||||
}
|
||||
|
||||
# Does ${1} merge cleanly onto the pristine upstream-synced main? Probed in a
|
||||
# throwaway worktree so the accumulated tree is left untouched. Tells apart a PR
|
||||
# that is simply stale against main (fixable inside that PR's own branch) from
|
||||
# one that only clashes with another open PR (needs a cross-PR decision).
|
||||
merges_onto_main() {
|
||||
local sha="$1" tmpdir probe rc=0
|
||||
tmpdir="$(mktemp -d)"
|
||||
probe="${tmpdir}/probe"
|
||||
git worktree add --quiet --detach "${probe}" "${MAIN_SYNCED}"
|
||||
if ! git -C "${probe}" merge --no-edit --no-ff -m probe "${sha}" > /dev/null 2>&1; then
|
||||
# Same policy as the real merge, or this misclassifies a lockfile-only clash.
|
||||
resolve_sole_lockfile "${probe}" > /dev/null 2>&1 || rc=1
|
||||
fi
|
||||
git worktree remove --force "${probe}" > /dev/null 2>&1 || true
|
||||
# worktree remove only takes the child back; without this the mktemp parent is left behind
|
||||
# on every checked conflict.
|
||||
rmdir "${tmpdir}" > /dev/null 2>&1 || true
|
||||
return "${rc}"
|
||||
}
|
||||
|
||||
git config --global user.email "addon-builder@users.noreply.github.com"
|
||||
git config --global user.name "BirdNET-Go Addon Builder"
|
||||
git config --global advice.detachedHead false
|
||||
@@ -121,7 +47,6 @@ git remote add upstream "${UPSTREAM_URL}"
|
||||
git fetch --no-tags upstream main
|
||||
# --no-ff keeps an explicit sync commit; a no-op when main is already current.
|
||||
git merge --no-edit --no-ff upstream/main
|
||||
MAIN_SYNCED="$(git rev-parse HEAD)"
|
||||
|
||||
log "Querying open non-draft PRs from ${FORK}"
|
||||
auth_header=()
|
||||
@@ -154,43 +79,27 @@ for entry in "${prs[@]}"; do
|
||||
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
||||
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
|
||||
|
||||
if resolve_sole_lockfile .; then
|
||||
: # generated lockfile only - the merge is committed and the build continues
|
||||
# package-lock.json is generated content and stacked PRs can carry an
|
||||
# older copy even when their source changes merge cleanly. Keep the
|
||||
# lockfile already assembled from upstream and earlier PRs, but only
|
||||
# when it is the sole conflict. Any source conflict remains fatal.
|
||||
if [ "${#conflicted_files[@]}" -eq 1 ] \
|
||||
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
|
||||
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
|
||||
git checkout --ours -- frontend/package-lock.json
|
||||
git add frontend/package-lock.json
|
||||
git commit --no-edit
|
||||
else
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
if [ "${#conflicted_files[@]}" -gt 0 ]; then
|
||||
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
|
||||
fi
|
||||
git merge --abort || true
|
||||
|
||||
if [ "${CHECK_ONLY}" = "1" ]; then
|
||||
scope="accumulated"
|
||||
merges_onto_main "${sha}" || scope="main"
|
||||
conflicting+=("${number}|${scope}|${conflicted_files[*]:-}|${title}")
|
||||
log "check mode: skipping PR #${number}, continuing with the rest"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${CHECK_ONLY}" = "1" ]; then
|
||||
if [ "${#conflicting[@]}" -eq 0 ]; then
|
||||
log "CHECK OK: every open non-draft PR merges into the combined build tree"
|
||||
exit 0
|
||||
fi
|
||||
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
|
||||
for entry in "${conflicting[@]}"; do
|
||||
IFS='|' read -r number scope files title <<<"${entry}"
|
||||
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
|
||||
done
|
||||
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
|
||||
echo "!!! conflicts-with=accumulated -> the PR only clashes with another open PR; decide which one owns the hunk." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
log "Merged HEAD: $(git rev-parse --short HEAD)"
|
||||
log "Source tree ready at ${TARGET_DIR}"
|
||||
|
||||
@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
|
||||
# config. (We can't remove the file and continue — subsequent yq calls
|
||||
# under set -e would abort the init script.)
|
||||
if ! curl -fL -s -S \
|
||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||
-o "$CONFIG_LOCATION"; then
|
||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||
-o "$CONFIG_LOCATION"; then
|
||||
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
||||
echo '{}' > "$CONFIG_LOCATION"
|
||||
fi
|
||||
@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
|
||||
if [ -d "$LOG_DIR" ]; then
|
||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||
ln -sf "$LOG_DIR" /logs
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
||||
fi
|
||||
|
||||
@@ -44,8 +44,8 @@ if ! bashio::config.true 'mariadb_auto_config'; then
|
||||
# Only revert if config.yaml points at the HA MariaDB host we would have
|
||||
# written — a mysql block pointing at a different host was set manually.
|
||||
# shellcheck disable=SC2016
|
||||
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2>/dev/null || true)"
|
||||
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \
|
||||
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)"
|
||||
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" > /dev/null 2>&1 \
|
||||
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
|
||||
yq -i -y \
|
||||
'.output.mysql.enabled = false
|
||||
@@ -71,7 +71,7 @@ bashio::log.green "---"
|
||||
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
|
||||
# gained IPv6, but the MariaDB addon only grants its user from the IPv4
|
||||
# subnet. Fall back to the raw hostname if resolution fails.
|
||||
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2>/dev/null | awk '{print $1; exit}')"
|
||||
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2> /dev/null | awk '{print $1; exit}')"
|
||||
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
|
||||
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
|
||||
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
## 20260827 (2026-08-29)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
- Upstream tag : 20260823
|
||||
@@ -6,13 +6,13 @@
|
||||
- Minor bugs fixed
|
||||
## 20260716.1 (26-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## 20260716.1 (2026-08-26)
|
||||
- Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests
|
||||
|
||||
## 20260716 (2026-07-16)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
|
||||
|
||||
## 20260712 (2026-07-13)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
## nightly-20260615-4 (09-07-2026)
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
## nightly-20260615-2 (05-07-2026)
|
||||
- Minor bugs fixed
|
||||
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
||||
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
||||
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
||||
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
||||
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
|
||||
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
|
||||
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)
|
||||
|
||||
@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
|
||||
# config. (We can't remove the file and continue — subsequent yq calls
|
||||
# under set -e would abort the init script.)
|
||||
if ! curl -fL -s -S \
|
||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||
-o "$CONFIG_LOCATION"; then
|
||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||
-o "$CONFIG_LOCATION"; then
|
||||
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
||||
echo '{}' > "$CONFIG_LOCATION"
|
||||
fi
|
||||
@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
|
||||
if [ -d "$LOG_DIR" ]; then
|
||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||
ln -sf "$LOG_DIR" /logs
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
||||
fi
|
||||
|
||||
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
||||
|
||||
**My recommendation:**
|
||||
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
|
||||
**Sources for high end microphones in Europe:**
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
|
||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||
log.info("Posted to MQTT: ok")
|
||||
|
||||
|
||||
# Create MQTT client using legacy callback API when available for
|
||||
# compatibility with paho-mqtt >= 2.0
|
||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
|
||||
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
||||
|
||||
**My recommendation:**
|
||||
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
|
||||
**Sources for high end microphones in Europe:**
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
|
||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||
log.info("Posted to MQTT: ok")
|
||||
|
||||
|
||||
# Create MQTT client using legacy callback API when available for
|
||||
# compatibility with paho-mqtt >= 2.0
|
||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
|
||||
## 0.8.8 (2026-08-22)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.8.6 (2026-08-13)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.8.5 (2026-08-01)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.8.4 (2026-07-13)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.8.3 (2026-07-11)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.8.2.1 (2026-07-05)
|
||||
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## 0.8.2 (2026-07-04)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
|
||||
## 0.7.5 (2026-06-23)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
## 0.7.4 (2026-06-01)
|
||||
|
||||
@@ -73,7 +73,7 @@ map:
|
||||
name: BirdNET-PiPy
|
||||
options:
|
||||
env_vars: []
|
||||
ICECAST_PASSWORD: ''
|
||||
ICECAST_PASSWORD: ""
|
||||
data_location: /config/data
|
||||
panel_icon: mdi:bird
|
||||
ports:
|
||||
|
||||
@@ -7,11 +7,11 @@ DATA_LOCATION="$(bashio::config 'data_location' || true)"
|
||||
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
|
||||
|
||||
case "${DATA_LOCATION}" in
|
||||
/config/*|/share/*|/data/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
|
||||
DATA_LOCATION="${DEFAULT_LOCATION}"
|
||||
;;
|
||||
/config/* | /share/* | /data/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
|
||||
DATA_LOCATION="${DEFAULT_LOCATION}"
|
||||
;;
|
||||
esac
|
||||
|
||||
LEGACY1="/config/birdnet-pipy/data"
|
||||
@@ -19,14 +19,14 @@ LEGACY2="/data"
|
||||
|
||||
mkdir -p "${DATA_LOCATION}"
|
||||
|
||||
if [ -z "$(ls -A "${DATA_LOCATION}" 2>/dev/null || true)" ]; then
|
||||
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2>/dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
|
||||
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2>/dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
|
||||
fi
|
||||
if [ -z "$(ls -A "${DATA_LOCATION}" 2> /dev/null || true)" ]; then
|
||||
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2> /dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
|
||||
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2> /dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true
|
||||
|
||||
@@ -7,10 +7,10 @@ CFG_DIR="${DATA_DIR}/config"
|
||||
SETTINGS="${CFG_DIR}/user_settings.json"
|
||||
|
||||
if [ -f "$SETTINGS" ]; then
|
||||
if ! grep -q 'detection' "$SETTINGS"; then
|
||||
bak="${SETTINGS}.bak"
|
||||
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mv -f -- "$SETTINGS" "$bak"
|
||||
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
|
||||
fi
|
||||
if ! grep -q 'detection' "$SETTINGS"; then
|
||||
bak="${SETTINGS}.bak"
|
||||
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mv -f -- "$SETTINGS" "$bak"
|
||||
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
set -euo pipefail
|
||||
|
||||
SND_GID=""
|
||||
if [ -e /dev/snd ] && command -v stat >/dev/null 2>&1; then
|
||||
SND_GID="$(stat -c '%g' /dev/snd 2>/dev/null || true)"
|
||||
if [ -e /dev/snd ] && command -v stat > /dev/null 2>&1; then
|
||||
SND_GID="$(stat -c '%g' /dev/snd 2> /dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -n "${SND_GID}" ] && getent group audio >/dev/null 2>&1; then
|
||||
current_gid="$(getent group audio | cut -d: -f3 || true)"
|
||||
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
|
||||
groupmod -g "${SND_GID}" audio 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${SND_GID}" ] && getent group audio > /dev/null 2>&1; then
|
||||
current_gid="$(getent group audio | cut -d: -f3 || true)"
|
||||
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
|
||||
groupmod -g "${SND_GID}" audio 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
for u in root nginx www-data icecast2; do
|
||||
if id "${u}" >/dev/null 2>&1; then
|
||||
usermod -aG audio "${u}" 2>/dev/null || true
|
||||
fi
|
||||
if id "${u}" > /dev/null 2>&1; then
|
||||
usermod -aG audio "${u}" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Create /run/pulse/native symlink if the PulseAudio socket is elsewhere
|
||||
@@ -25,21 +25,21 @@ done
|
||||
PULSE_SOCK="${PULSE_SERVER:-}"
|
||||
PULSE_SOCK="${PULSE_SOCK#unix:}"
|
||||
if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then
|
||||
mkdir -p /run/pulse
|
||||
ln -sf "${PULSE_SOCK}" /run/pulse/native
|
||||
mkdir -p /run/pulse
|
||||
ln -sf "${PULSE_SOCK}" /run/pulse/native
|
||||
fi
|
||||
|
||||
# Copy PulseAudio cookie for the icecast2 user so it can authenticate
|
||||
if id icecast2 >/dev/null 2>&1; then
|
||||
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
|
||||
if [ -n "${ICECAST_HOME}" ]; then
|
||||
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
|
||||
if [ -f "${cookie}" ]; then
|
||||
mkdir -p "${ICECAST_HOME}/.config/pulse"
|
||||
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if id icecast2 > /dev/null 2>&1; then
|
||||
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
|
||||
if [ -n "${ICECAST_HOME}" ]; then
|
||||
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
|
||||
if [ -f "${cookie}" ]; then
|
||||
mkdir -p "${ICECAST_HOME}/.config/pulse"
|
||||
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
14
birdnet-pipy/rootfs/etc/services.d/api/run
Normal file → Executable file
14
birdnet-pipy/rootfs/etc/services.d/api/run
Normal file → Executable file
@@ -10,10 +10,10 @@ cd /app
|
||||
# and core.main reach the API locally, satisfying the wait_for in nginx/run +
|
||||
# main/run). Heavy admin jobs cooperatively yield to keep this worker live.
|
||||
exec gunicorn wsgi:application \
|
||||
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
||||
--workers 1 \
|
||||
--bind 127.0.0.1:5002 \
|
||||
--timeout 120 \
|
||||
--graceful-timeout 30 \
|
||||
--keep-alive 65 \
|
||||
--error-logfile -
|
||||
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
||||
--workers 1 \
|
||||
--bind 127.0.0.1:5002 \
|
||||
--timeout 120 \
|
||||
--graceful-timeout 30 \
|
||||
--keep-alive 65 \
|
||||
--error-logfile -
|
||||
|
||||
0
birdnet-pipy/rootfs/etc/services.d/icecast/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/icecast/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/main/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/main/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/model/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/model/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/nginx/run
Normal file → Executable file
@@ -1,10 +1,10 @@
|
||||
|
||||
|
||||
## 1.37.2 (2026-08-29)
|
||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||
|
||||
|
||||
## 1.37.1 (2026-08-01)
|
||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||
|
||||
|
||||
## 1.37.0 (2026-07-25)
|
||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||
|
||||
|
||||
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Normal file → Executable file
6
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/vaultwarden/run
Normal file → Executable file
6
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/vaultwarden/run
Normal file → Executable file
@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
|
||||
# Find the matching log level
|
||||
if bashio::config.has_value 'log_level'; then
|
||||
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
|
||||
all|trace)
|
||||
all | trace)
|
||||
log_level="trace"
|
||||
;;
|
||||
debug)
|
||||
log_level="debug"
|
||||
;;
|
||||
info|notice)
|
||||
info | notice)
|
||||
log_level="info"
|
||||
;;
|
||||
warning)
|
||||
log_level="warn"
|
||||
;;
|
||||
error|fatal)
|
||||
error | fatal)
|
||||
log_level="error"
|
||||
;;
|
||||
off)
|
||||
|
||||
@@ -64,7 +64,7 @@ Default credentials are shown in the startup logs.
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
|
||||
|
||||
## 1.94.121-ls127 (2026-09-05)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.94.117-ls125 (2026-08-29)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.93.138-ls124 (2026-08-22)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.93.136-ls122 (2026-08-13)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.93.134-ls121 (2026-08-08)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.93.129-ls119 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.92.144-ls117 (2026-07-25)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.92.141-ls115 (2026-07-18)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.92.140-ls113 (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.92.139-ls112 (2026-07-11)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.92.134-ls109 (2026-07-04)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.91.180-ls108 (2026-06-27)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.91.175-ls106 (2026-06-20)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.91.172-ls105 (2026-06-13)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
## 1.91.171-ls104 (2026-06-11)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
## 1.91.168-ls103 (2026-06-05)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
|
||||
## 2026.09.05 (2026-09-05)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
- Upstream tag : version-d39975b5
|
||||
|
||||
|
||||
## 2026.08.13 (2026-08-13)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
- Upstream tag : version-e8713bf7
|
||||
|
||||
|
||||
## 2026.08.01.1 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
- Upstream tag : version-e2e1ec9c
|
||||
@@ -14,25 +14,25 @@
|
||||
|
||||
- Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged
|
||||
|
||||
|
||||
|
||||
## version-bf9e0b4f (2026-07-25)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-ae9a7fca (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-30a7c401 (2026-07-11)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-7148c2a3 (2026-07-04)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-ec868334 (2026-06-27)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-b9b82d3e (2026-06-20)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
## version-cca5154d (2026-06-11)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
## version-89f88890 (2026-06-05)
|
||||
|
||||
1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh
Executable file → Normal file
1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh
Executable file → Normal file
@@ -7,7 +7,6 @@ set -e
|
||||
PUID=$(bashio::config "PUID")
|
||||
PGID=$(bashio::config "PGID")
|
||||
|
||||
|
||||
# Create cache
|
||||
#mkdir -p /.cache
|
||||
#chmod 755 /.cache
|
||||
|
||||
@@ -5,12 +5,12 @@ set -Eeuo pipefail
|
||||
|
||||
# Resolve chromium binary (LSIO uses this path)
|
||||
if [[ -x /usr/lib/chromium/chromium ]]; then
|
||||
BIN=/usr/lib/chromium/chromium
|
||||
BIN=/usr/lib/chromium/chromium
|
||||
elif command -v chromium >/dev/null 2>&1; then
|
||||
BIN="$(command -v chromium)"
|
||||
BIN="$(command -v chromium)"
|
||||
else
|
||||
echo "ERROR: chromium binary not found" >&2
|
||||
exit 127
|
||||
echo "ERROR: chromium binary not found" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
# Fixed profile dir as requested
|
||||
@@ -19,7 +19,7 @@ mkdir -p "${USER_DATA_DIR}"
|
||||
|
||||
# Cleanup stale locks if chromium is not already running
|
||||
if ! pgrep -f 'chromium' >/dev/null 2>&1; then
|
||||
rm -f "${USER_DATA_DIR}/Singleton"* || true
|
||||
rm -f "${USER_DATA_DIR}/Singleton"* || true
|
||||
fi
|
||||
|
||||
# Detect seccomp mode (0 = disabled)
|
||||
@@ -27,22 +27,22 @@ seccomp_mode="$(awk -F':' '/^Seccomp:/{gsub(/[[:space:]]/,"",$2); print $2}' /pr
|
||||
|
||||
# Base arguments (exactly what you requested + required safety flags)
|
||||
args=(
|
||||
--remote-debugging-address=0.0.0.0
|
||||
--remote-debugging-port=9221
|
||||
--user-data-dir="${USER_DATA_DIR}"
|
||||
--disable-dev-shm-usage
|
||||
--no-first-run
|
||||
--no-default-browser-check
|
||||
--disable-background-networking
|
||||
--disable-sync
|
||||
--password-store=basic
|
||||
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
|
||||
--start-maximized
|
||||
--remote-debugging-address=0.0.0.0
|
||||
--remote-debugging-port=9221
|
||||
--user-data-dir="${USER_DATA_DIR}"
|
||||
--disable-dev-shm-usage
|
||||
--no-first-run
|
||||
--no-default-browser-check
|
||||
--disable-background-networking
|
||||
--disable-sync
|
||||
--password-store=basic
|
||||
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
|
||||
--start-maximized
|
||||
)
|
||||
|
||||
# Container reality: sandbox usually breaks unless seccomp is fully disabled
|
||||
if [[ "${seccomp_mode}" != "0" ]]; then
|
||||
args+=( --no-sandbox --test-type )
|
||||
args+=(--no-sandbox --test-type)
|
||||
fi
|
||||
|
||||
# IMPORTANT:
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
|
||||
|
||||
## 2.56.3 (2026-09-05)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.56.0 (2026-08-22)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.55.4 (2026-08-13)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.55.3 (2026-08-08)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.55.2 (2026-08-01)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.55.0 (2026-07-16)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.54.2 (2026-07-04)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.54.1 (2026-06-23)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.54.0 (2026-06-20)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.52.2 (2026-06-17)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.52.1 (2026-06-13)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
## 2.52.0 (2026-06-11)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
## 2.51.0 (2026-06-05)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
|
||||
|
||||
## 9.14.0 (2026-08-29)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
|
||||
|
||||
## 9.13.0 (2026-08-08)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
|
||||
|
||||
## 9.12.0 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
|
||||
|
||||
## 9.11.0 (2026-07-04)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
|
||||
|
||||
## 9.10.0 (2026-06-27)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
## 0.6.27.4 (2026-09-04)
|
||||
- Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040)
|
||||
- Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone
|
||||
|
||||
@@ -62,7 +62,7 @@ Default password: admin123
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
@@ -99,7 +99,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Optional Calibre-Web features
|
||||
|
||||
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` *and* its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
||||
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` _and_ its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
||||
|
||||
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ else
|
||||
# duplicates it can leave behind are entries calibre-web skips or already trusts.
|
||||
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
|
||||
# before calibre-web, so a failure here is not fatal : the next start applies it.
|
||||
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) ||
|
||||
bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
||||
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) \
|
||||
|| bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
||||
|
||||
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install
|
||||
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and
|
||||
# stored an empty string, so it is never retried. Put an empty value back to NULL and
|
||||
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
|
||||
# A path the user set by hand is not empty and is left alone.
|
||||
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) ||
|
||||
bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
||||
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) \
|
||||
|| bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
||||
fi
|
||||
|
||||
bashio::log.info "Default username:password is admin:admin123"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
## 0.60.3 (2026-09-05)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
|
||||
## 0.55.8 (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ You can add a shortcut pointing to your Changedetection.io instance with the fol
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
0
changedetection.io/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
changedetection.io/rootfs/etc/services.d/nginx/run
Normal file → Executable file
@@ -1,12 +1,12 @@
|
||||
|
||||
|
||||
## 07308545.6 (2026-09-05)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.3+claude1.40609.1
|
||||
|
||||
|
||||
## 07308545.5 (2026-08-29)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.2+claude1.37937.3
|
||||
|
||||
|
||||
## 07308545.4 (2026-08-22)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.2+claude1.32885.1
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
## 07308545.1 (17-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## 07308543.1 (17-08-2026)
|
||||
- Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37
|
||||
`safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how
|
||||
@@ -94,11 +94,11 @@
|
||||
## 07308545 (2026-08-15)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.2+claude1.30096.1
|
||||
|
||||
|
||||
## 07308544 (2026-08-13)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.2+claude1.28929.0
|
||||
|
||||
|
||||
## 07308543 (2026-08-08)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
- Upstream tag : debiantrixie-version-07308543
|
||||
@@ -185,10 +185,10 @@
|
||||
|
||||
## 2026.08.02 (02-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## ubunturesolute-version-3a10bef7 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
|
||||
|
||||
## kali-version-e963b19b (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
## 1.37 (28-07-2026)
|
||||
@@ -199,7 +199,7 @@
|
||||
- **`gnome-keyring` stays out of the image.** Re-adding it would reintroduce the first-boot keyring password prompt that blocks Claude Desktop from launching at all. This route needs no keyring, no D-Bus Secret Service, no daemon and no password, so no prompt can appear. The trade-off is unchanged from v1.35 and is inherent to the `basic` backend: its key is fixed rather than gated by a keyring, so any process that can read the persistent `$HOME/.config/Claude` profile can recover the stored credentials.
|
||||
- **One-time step after upgrading.** The previously stored session is already stale, so a single sign-in is still needed once after this update; it then persists across restarts.
|
||||
|
||||
|
||||
|
||||
## 1.36.4 (28-07-2026)
|
||||
|
||||
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
|
||||
@@ -210,7 +210,7 @@
|
||||
|
||||
## 1.36.2 (28-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## 1.36.1 (27-07-2026)
|
||||
|
||||
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it.
|
||||
@@ -240,10 +240,10 @@
|
||||
## 1.33 (22-07-2026)
|
||||
|
||||
- Add cowork virtualization support: `qemu-system-x86` and `ovmf` (Bookworm main, installed via apt) plus `virtiofsd` for sharing the workspace into the sandbox microVM. `virtiofsd` is only packaged for Debian trixie/sid, not Bookworm or bookworm-backports, and its trixie `.deb` links a newer GLIBC than this add-on's Bookworm runtime — so it now gets built from the pinned crates.io release (`1.14.0`) in a dedicated `virtiofsd-builder` stage, the same GLIBC-safe pattern already used for `rtk` and `tokensave`. Its build deps (`libseccomp-dev`, `libcap-ng-dev`, `pkg-config`, `clang`, `libclang-dev`) live only in that builder stage; only the runtime shared libs (`libseccomp2`, `libcap-ng0`) ship in the final image. The built binary is validated with `--version` at build time alongside `rtk`/`tokensave`, so a GLIBC/ABI mismatch fails the image build instead of surfacing at container start. Docker itself is deliberately not installed: this base image already ships Docker-in-Docker (`docker-ce`/`containerd.io` from Docker's own apt repo, started via the pre-existing `START_DOCKER` env var) — an initial attempt to also `apt-get install docker.io` broke the build, since Debian's package pulls in `containerd`/`runc`, which apt refuses to install alongside the base image's already-installed `containerd.io` (`Conflicts`).
|
||||
|
||||
|
||||
## ubunturesolute-version-8208e985 (2026-07-21)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
|
||||
|
||||
## kali-version-9ad48e7a (2026-07-18)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
## 1.32 (17-07-2026)
|
||||
|
||||
@@ -55,9 +55,9 @@
|
||||
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the
|
||||
# flag — the crash it prevents is worse than the overhead it costs.
|
||||
SHM_FLAGS="--disable-dev-shm-usage"
|
||||
SHM_KB="$(df -k /dev/shm 2> /dev/null | awk 'NR==2 {print $2}')"
|
||||
SHM_KB="$(df -k /dev/shm 2>/dev/null | awk 'NR==2 {print $2}')"
|
||||
if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
|
||||
SHM_FLAGS=""
|
||||
SHM_FLAGS=""
|
||||
fi
|
||||
|
||||
# SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all.
|
||||
|
||||
@@ -141,7 +141,7 @@ release_info=""
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
|
||||
-o "$release_metadata" "$CODEX_RELEASE_API"; then
|
||||
release_info="$(
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" << 'PY' 2> /dev/null || true
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -249,7 +249,7 @@ fi
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
|
||||
cat <<'SH'
|
||||
cat << 'SH'
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex: unable to resolve the abc runtime home" >&2
|
||||
@@ -338,8 +338,7 @@ ln -sfn "$CODEX_BIN" "$CODEX_LINK"
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
|
||||
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
|
||||
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
|
||||
run_as_runtime_user python3 - <<'PY' \
|
||||
|| bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
run_as_runtime_user python3 - << 'PY' || bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
@@ -30,7 +30,7 @@ manage_claude_md_block() {
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - << 'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -58,7 +58,7 @@ PY
|
||||
manage_settings_hook() {
|
||||
# manage_settings_hook <event> <matcher> <command> <add|remove>
|
||||
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||
python3 - << 'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -389,14 +389,14 @@ bashio::log.info "MCP servers for Claude Desktop: ${MCP_SERVERS_DESKTOP}"
|
||||
bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
|
||||
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - << 'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -607,7 +607,7 @@ PY
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable.
|
||||
if $HEADROOM_ENABLED; then
|
||||
manage_claude_md_block headroom add <<'MD'
|
||||
manage_claude_md_block headroom add << 'MD'
|
||||
## Headroom context compression
|
||||
|
||||
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
||||
@@ -636,7 +636,7 @@ if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
else
|
||||
HEADROOM_ROUTE_ACTION="remove"
|
||||
fi
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - << 'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -703,7 +703,7 @@ manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD"
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed).
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
manage_claude_md_block ha-api-helper add <<'MD'
|
||||
manage_claude_md_block ha-api-helper add << 'MD'
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
@@ -731,7 +731,7 @@ fi
|
||||
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
|
||||
# for a second agent, the same gap the Headroom block above exists to close.
|
||||
if $CODEX_ENABLED; then
|
||||
manage_claude_md_block codex add <<'MD'
|
||||
manage_claude_md_block codex add << 'MD'
|
||||
## Delegating to ChatGPT Codex
|
||||
|
||||
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
|
||||
|
||||
@@ -19,7 +19,7 @@ esac
|
||||
# only while it still holds one of those managed values — a defaultMode the user set by hand
|
||||
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
# archive stays unpatched, so every later boot would retry under a new pid and strand another
|
||||
# copy until the container runs out of space.
|
||||
cleanup_tmp() {
|
||||
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2>/dev/null || true
|
||||
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2> /dev/null || true
|
||||
}
|
||||
|
||||
if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then
|
||||
|
||||
@@ -45,7 +45,7 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -66,7 +66,7 @@ else:
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -104,7 +104,7 @@ for path in paths:
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
0
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file → Normal file
0
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file → Normal file
@@ -96,7 +96,7 @@ def resolve_endpoint():
|
||||
def _url(base, path):
|
||||
path = path.lstrip("/")
|
||||
if path.startswith("api/"):
|
||||
path = path[len("api/"):]
|
||||
path = path[len("api/") :]
|
||||
return base.rstrip("/") + "/" + path
|
||||
|
||||
|
||||
@@ -133,7 +133,9 @@ def rest(method, base, token, path, body=None):
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
|
||||
req = urllib.request.Request(
|
||||
_url(base, path), data=data, method=method, headers=headers
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode("utf-8")
|
||||
@@ -141,7 +143,9 @@ def rest(method, base, token, path, body=None):
|
||||
detail = exc.read().decode("utf-8", "replace").strip()
|
||||
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
|
||||
sys.exit(
|
||||
f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}"
|
||||
)
|
||||
try:
|
||||
return json.loads(text) if text.strip() else None
|
||||
except ValueError:
|
||||
@@ -214,7 +218,9 @@ def main(argv):
|
||||
sys.exit("usage: ha-cli call <domain.service> [BODY]")
|
||||
domain, service = args[0].split(".", 1)
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
|
||||
_print(
|
||||
rest("POST", rest_base, token, f"services/{domain}/{service}", body or {})
|
||||
)
|
||||
elif cmd == "states":
|
||||
path = f"states/{args[0]}" if args else "states"
|
||||
_print(rest("GET", rest_base, token, path))
|
||||
|
||||
14
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file → Normal file
@@ -76,7 +76,11 @@ def main() -> int:
|
||||
|
||||
# Find big string/array fields before paying the headroom import cost.
|
||||
def is_string_array(value):
|
||||
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > ARRAY_KEEP
|
||||
and all(isinstance(v, str) for v in value)
|
||||
)
|
||||
|
||||
if isinstance(response, str):
|
||||
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
|
||||
@@ -85,10 +89,14 @@ def main() -> int:
|
||||
string_candidates = [
|
||||
key
|
||||
for key, value in response.items()
|
||||
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
|
||||
if key not in SKIP_KEYS
|
||||
and isinstance(value, str)
|
||||
and len(value) >= MIN_CHARS
|
||||
]
|
||||
array_candidates = [
|
||||
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
|
||||
key
|
||||
for key, value in response.items()
|
||||
if key not in SKIP_KEYS and is_string_array(value)
|
||||
]
|
||||
else:
|
||||
string_candidates = []
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
|
||||
|
||||
## 2.10.5 (2026-08-13)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
|
||||
## 2.10.3 (2026-08-08)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
|
||||
## 2.10.2 (2026-08-01)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
|
||||
## 2.9.16 (2026-07-11)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
|
||||
## 2.9.14 (2026-06-20)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Integrations supported:
|
||||
## Configuration
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| -------- | ------------- |
|
||||
| `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. |
|
||||
| `PUID` | User ID to run the process as. Defaults to `0` (root). |
|
||||
| `PGID` | Group ID to run the process as. Defaults to `0` (root). |
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
description:
|
||||
Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
description: Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
|
||||
@@ -33,7 +33,7 @@ mkdir -p "$HA_DATA_DIR"
|
||||
# Symlink /app/config → HA persistent storage
|
||||
# The .NET app uses /app/config, NOT /config at the filesystem root
|
||||
if [ -d /app/config ] && [ ! -L /app/config ]; then
|
||||
cp -rn /app/config/. "$HA_DATA_DIR/" 2>/dev/null || true
|
||||
cp -rn /app/config/. "$HA_DATA_DIR/" 2> /dev/null || true
|
||||
rm -rf /app/config
|
||||
fi
|
||||
ln -sfn "$HA_DATA_DIR" /app/config
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
|
||||
## 19.20.5 (2026-09-05)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
|
||||
|
||||
## 19.20.4 (2026-08-29)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
|
||||
|
||||
## 19.20.0 (2026-08-08)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
|
||||
|
||||
## 19.19.1 (2026-06-17)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `CUSTOM_OPTIONS` | str | | Custom CLI options (e.g., `--name Homeassistant`) |
|
||||
| `DROPBOX_TOKEN` | str | | Dropbox integration token (see https://cloudcmd.io/) |
|
||||
| `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) |
|
||||
|
||||
@@ -103,7 +103,7 @@ for candidate in "${cloudcmd_candidates[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd >/dev/null 2>&1; then
|
||||
if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd > /dev/null 2>&1; then
|
||||
CLOUDCMD_BIN=$(command -v cloudcmd)
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
|
||||
|
||||
## 2.2.11 (2026-08-29)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.8 (2026-08-22)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.6 (2026-08-08)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.5 (2026-08-02)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.4 (2026-08-01)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.2 (2026-07-25)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.2.0 (2026-07-16)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.1.2 (2026-07-11)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.1.0 (2026-07-04)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.0.3 (2026-06-23)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 2.0.0 (2026-06-20)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
## 1.12.8 (2026-06-17)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ You can place the user folder from the theme/skeleton in /share/codex/www/user,
|
||||
## Options
|
||||
|
||||
| Option | Description | Default | Example |
|
||||
|--------|-------------|---------|---------|
|
||||
| -------- | ------------- | --------- | --------- |
|
||||
| `PGID` | Group ID for file permissions | `0` | `1000` |
|
||||
| `PUID` | User ID for file permissions | `0` | `1000` |
|
||||
| `TZ` | Timezone in long format | - | `America/Los_Angeles` |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
|
||||
## 26.04.3.2.1 (2026-09-05)
|
||||
- Update to latest version from collabora/code
|
||||
|
||||
|
||||
## 26.04.3.1.1 (2026-08-13)
|
||||
- Update to latest version from collabora/code
|
||||
|
||||
|
||||
## 26.04.2.4.1 (2026-07-26)
|
||||
- Rebuild on a Debian base: upstream turned collabora/code into a distroless image with no shell, which broke the addon build entirely. collabora/code stays the tracked upstream image in build.json, but is now a build stage whose payload is copied onto ghcr.io/hassio-addons/debian-base, and the addon ships its own launcher in place of the removed /start-collabora-online.sh
|
||||
- build.json names the architecture explicitly again (`collabora/code:latest-amd64` and `collabora/code:latest-arm64`). The builder never passes `--platform`, so the tag is the only thing that decides which binaries land in the addon
|
||||
@@ -19,16 +19,16 @@
|
||||
- Added `aliasgroup2` and `aliasgroup3` for additional Nextcloud servers
|
||||
- `cert_domain` is now a string (it is a certificate common name) and is passed to Collabora
|
||||
- Documented the above, and corrected the README which asked for two backslashes where Collabora expects one
|
||||
|
||||
|
||||
## 1.3.0 (2026-07-16)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
## 1.2.2 (2026-07-04)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
## 1.2.1 (2026-06-27)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
## 1.2.0 (2026-06-23)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
|
||||
| `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` |
|
||||
| `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user