diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index f6e94f9..bdccdb0 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -836,10 +836,14 @@ function vv_log_tail(string $path, int $lines): string { return implode("\n", array_slice($all, -$lines)); } +// See VV_CONF_ARRAY_BODY in config.php for why this cannot stop at the first `)`. function vv_parse_bash_array(string $raw, string $varName): array { - if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(([^)]*)\)/ms', $raw, $m)) return []; + if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(' . VV_CONF_ARRAY_BODY . '/ms', + $raw, $m)) return []; + // Group 2 exists only when the multi-line branch matched; group 1 is the single-line body. + $body = isset($m[2]) ? $m[2] : ($m[1] ?? ''); $items = []; - foreach (explode("\n", $m[1]) as $line) { + foreach (explode("\n", $body) as $line) { $line = trim(preg_replace('/#.*$/', '', $line), " \t\"'"); if ($line !== '') $items[] = $line; } diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php index 8db3519..118904d 100644 --- a/Plugin/unraid/include/confform.php +++ b/Plugin/unraid/include/confform.php @@ -813,16 +813,18 @@ function vv_conf_write_file(string $file, array $fileChanges): bool { $raw ) ?? $raw; + // VV_CONF_ARRAY_CLOSE, not [^)]*\) — see the constant for what the greedy-to-first-paren + // version did to a list whose comments contain one. } elseif ($type === 'array') { $raw = preg_replace_callback( - '/^(\s*)(' . $qKey . '\s*=\s*\()[^)]*\)/ms', + '/^(\s*)(' . $qKey . '\s*=\s*\()' . VV_CONF_ARRAY_CLOSE . '/ms', fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")", $raw ) ?? $raw; } elseif ($type === 'assoc_array') { $raw = preg_replace_callback( - '/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()[^)]*\)/ms', + '/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()' . VV_CONF_ARRAY_CLOSE . '/ms', fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")", $raw ) ?? $raw; diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index 09bdc86..2f5e939 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -645,6 +645,31 @@ function vv_format_uptime(int $seconds): string { return ($d ? "{$d}d " : '') . ($h ? "{$h}h " : '') . "{$m}m"; } +// ── Where a bash array ends ────────────────────────────────────────────────────────────────── +// Both halves of this had the same bug independently, which is why the rule now lives in one +// place: `[^)]*\)` — run to the first closing paren — is only the array's own close if no entry +// or comment contains one. HOST1_WATCHDOG_SCAN_IGNORE has carried +// `# broken test container (exit 127 — bad image)` for weeks, and the two sides failed differently +// against it: +// +// writing vv_conf_write_file() spliced the new body into the middle of that comment and left +// the real `)` stranded below as a stray token. `bash -n` caught it and vv_conf_edit() +// rolled the file back, so no conf was corrupted — but the caller got a bare `false` +// with an empty $rejected, and every future save of that list would have failed the +// same silent way. +// reading vv_parse_bash_array() stopped there and returned the entries above it, so the array +// was quietly short. Only the PHP layer was affected: bash sources the file itself and +// always saw every entry, so the watchdogs behaved correctly while the pages under- +// reported what they were configured with — the failure mode with no symptom. +// +// confform.php's own reader was always right, and this is its rule: a multi-line array closes on +// a `)` that starts its own line. Parens anywhere else are just text. +// +// CLOSE is non-capturing, for splicing a new body in. BODY captures — group 1 when the array is +// written on one line, group 2 when it spans several. +const VV_CONF_ARRAY_CLOSE = '(?:[^)\n]*\)|.*?\n[ \t]*\))'; +const VV_CONF_ARRAY_BODY = '(?:([^)\n]*)\)|\n(.*?)^[ \t]*\)[ \t]*$)'; + // Parse a scalar value from raw conf text. Matches KEY="value" or KEY=value. // Identical logic was previously duplicated as vv_arr_scalar / vv_wd_scalar / // vv_fb_scalar / vv_media_conf_scalar — all reduce to this one regex.