Add proxy subscription runtime support
Add daemon RPCs, Apple and GTK import flows, packet proxy runtime support, diagnostics, and BEPs for proxy subscription handling. Redact subscription URL secrets from fetch errors before they reach logs or UI surfaces.
This commit is contained in:
parent
97c569fb35
commit
d1638726ca
46 changed files with 15079 additions and 456 deletions
434
Scripts/burrow-doctor
Executable file
434
Scripts/burrow-doctor
Executable file
|
|
@ -0,0 +1,434 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/burrow-doctor [--last 10m]
|
||||
|
||||
Print a single stdout diagnostics report for the CURRENT macOS networking state.
|
||||
|
||||
Run this while Burrow is the active VPN but routing/websites are broken, then
|
||||
redirect stdout to a file before switching to a working VPN:
|
||||
|
||||
Scripts/burrow-doctor --last 10m > burrow-broken.txt
|
||||
|
||||
The report focuses on live VPN/proxy/DNS/route state, Burrow NetworkExtension
|
||||
state, socket ownership, the selected Burrow node, and recent Burrow logs.
|
||||
It intentionally does not redact local output, because broken proxy state often
|
||||
depends on exact subscription and node configuration.
|
||||
EOF
|
||||
}
|
||||
|
||||
last="10m"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--last)
|
||||
[[ $# -ge 2 ]] || { echo "--last requires a value" >&2; exit 2; }
|
||||
last="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
group_id="${BURROW_APP_GROUP_ID:-2KPBQHRJ2D.com.tianruichen.burrow}"
|
||||
group_dir="${BURROW_APP_GROUP_DIR:-$HOME/Library/Group Containers/$group_id}"
|
||||
db_path="${BURROW_DB_PATH:-$group_dir/burrow.db}"
|
||||
runtime_log="${BURROW_RUNTIME_LOG:-$group_dir/burrow-runtime.log}"
|
||||
bundle_id="${BURROW_BUNDLE_ID:-com.tianruichen.burrow}"
|
||||
extension_process="${BURROW_EXTENSION_PROCESS:-BurrowNetworkExtension}"
|
||||
|
||||
section() {
|
||||
printf '\n## %s\n\n' "$1"
|
||||
}
|
||||
|
||||
cmd() {
|
||||
printf '$'
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
run() {
|
||||
local title="$1"
|
||||
shift
|
||||
section "$title"
|
||||
cmd "$@"
|
||||
printf '\n'
|
||||
"$@" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?"
|
||||
}
|
||||
|
||||
run_zsh() {
|
||||
local title="$1"
|
||||
local script="$2"
|
||||
section "$title"
|
||||
printf '$ %s\n\n' "$script"
|
||||
/bin/zsh -lc "$script" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?"
|
||||
}
|
||||
|
||||
run_python() {
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
uv run python "$@"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
python3 "$@"
|
||||
else
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
capture() {
|
||||
"$@" 2>&1 || true
|
||||
}
|
||||
|
||||
nc_list="$(capture scutil --nc list)"
|
||||
proxy_state="$(capture scutil --proxy)"
|
||||
dns_state="$(capture scutil --dns)"
|
||||
inet_routes="$(capture netstat -rn -f inet)"
|
||||
proxy_listeners="$(capture /bin/zsh -lc "lsof -nP -iTCP:6152 -iTCP:6153 -iTCP:7890 -iTCP:7891 -iTCP:7897 -iTCP:1080 -iTCP:1087 -sTCP:LISTEN")"
|
||||
|
||||
cat <<EOF
|
||||
# Burrow Doctor
|
||||
|
||||
Generated: $(date)
|
||||
Repo: $repo_root
|
||||
Log window: $last
|
||||
App group: $group_id
|
||||
DB path: $db_path
|
||||
Runtime log: $runtime_log
|
||||
|
||||
Purpose: snapshot CURRENT networking state while Burrow is active but not
|
||||
routing requests, so this report can be inspected later after switching to a
|
||||
working VPN.
|
||||
EOF
|
||||
|
||||
section "Quick Findings"
|
||||
if printf '%s\n' "$nc_list" | grep -Eiq '\(Connected\).+Burrow'; then
|
||||
echo "- Burrow is currently connected according to scutil."
|
||||
else
|
||||
echo "- Burrow is NOT currently connected according to scutil. If this was not captured during the broken active-Burrow window, rerun it while Burrow is active."
|
||||
fi
|
||||
|
||||
other_connected="$(printf '%s\n' "$nc_list" | grep -Ei '\(Connected\)' | grep -Eiv 'Burrow|MT65xx' || true)"
|
||||
if [[ -n "$other_connected" ]]; then
|
||||
echo "- Another VPN-like service is connected and may supersede or interfere with Burrow:"
|
||||
printf '%s\n' "$other_connected" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$proxy_state" | grep -Eq 'HTTPProxy : 127\.0\.0\.1|SOCKSProxy : 127\.0\.0\.1|HTTPSProxy : 127\.0\.0\.1'; then
|
||||
echo "- System proxy settings point to localhost. See Local Proxy Listeners for the owning process."
|
||||
fi
|
||||
|
||||
if printf '%s\n%s\n' "$dns_state" "$inet_routes" | grep -Eq '198\.18\.|198\.19\.'; then
|
||||
echo "- Current DNS/routes include 198.18.0.0/15 fake-IP style addresses."
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$inet_routes" | grep -Eq '^default[[:space:]].*utun'; then
|
||||
echo "- IPv4 default route currently points at a utun interface."
|
||||
fi
|
||||
|
||||
if [[ -n "$proxy_listeners" ]]; then
|
||||
echo "- Common localhost proxy ports are listening:"
|
||||
printf '%s\n' "$proxy_listeners" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
if [[ -f "$runtime_log" ]] && tail -400 "$runtime_log" | grep -q 'panicked'; then
|
||||
echo "- Burrow runtime log contains recent task panics. See Burrow Runtime Log File."
|
||||
fi
|
||||
|
||||
if [[ -f "$runtime_log" ]] && tail -400 "$runtime_log" | grep -q 'CryptoProvider'; then
|
||||
echo "- Burrow runtime log mentions rustls CryptoProvider initialization failures."
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$dns_state" | grep -A8 '^resolver #1' | grep -Eq 'if_index[[:space:]]*:.*utun' \
|
||||
&& [[ -f "$runtime_log" ]] \
|
||||
&& tail -400 "$runtime_log" | grep -q 'proxy udp session failed'; then
|
||||
echo "- Primary DNS is on the tunnel while Burrow proxy UDP sessions are failing; name resolution may be blocked before HTTP/TCP requests can start."
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "$dns_state" | awk '
|
||||
/^DNS configuration$/ { global = 1; next }
|
||||
/^DNS configuration \(for scoped queries\)/ { global = 0; next }
|
||||
global && /nameserver\[[0-9]+\]/ { found = 1 }
|
||||
END { exit found ? 0 : 1 }
|
||||
'; then
|
||||
echo "- No global DNS nameserver is configured. If only scoped physical DNS remains, ordinary hostname lookups may fail under the full-tunnel route."
|
||||
fi
|
||||
|
||||
section "Selected Burrow Network"
|
||||
if [[ -f "$db_path" ]]; then
|
||||
run_python - "$db_path" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
db_path = sys.argv[1]
|
||||
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = list(conn.execute("select id,type,idx,payload,interface_id from network order by idx"))
|
||||
if not rows:
|
||||
print("No rows in network table.")
|
||||
raise SystemExit
|
||||
|
||||
summaries = []
|
||||
for row in rows:
|
||||
payload_blob = row["payload"]
|
||||
payload_text = payload_blob.decode("utf-8", errors="replace") if isinstance(payload_blob, bytes) else payload_blob
|
||||
try:
|
||||
payload = json.loads(payload_text) if payload_text else {}
|
||||
except Exception as exc:
|
||||
summaries.append({
|
||||
"id": row["id"],
|
||||
"type": row["type"],
|
||||
"idx": row["idx"],
|
||||
"payload_parse_error": str(exc),
|
||||
})
|
||||
continue
|
||||
|
||||
summary = {
|
||||
"id": row["id"],
|
||||
"type": row["type"],
|
||||
"idx": row["idx"],
|
||||
"interface_id": row["interface_id"],
|
||||
"name": payload.get("name"),
|
||||
"selected_ordinal": payload.get("selected_ordinal"),
|
||||
"selected_name": payload.get("selected_name"),
|
||||
"source": payload.get("source"),
|
||||
"node_count": len(payload.get("nodes") or []),
|
||||
}
|
||||
selected_node = None
|
||||
for node in payload.get("nodes") or []:
|
||||
if node.get("name") == payload.get("selected_name") or node.get("ordinal") == payload.get("selected_ordinal"):
|
||||
selected_node = node
|
||||
break
|
||||
summary["selected_node"] = selected_node
|
||||
summaries.append(summary)
|
||||
|
||||
print(json.dumps(summaries, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
else
|
||||
echo "DB not found: $db_path"
|
||||
fi
|
||||
|
||||
run "VPN Configuration List" scutil --nc list
|
||||
run "System Proxy State" scutil --proxy
|
||||
run_zsh "Local Proxy Listeners" "lsof -nP -iTCP:6152 -iTCP:6153 -iTCP:7890 -iTCP:7891 -iTCP:7897 -iTCP:1080 -iTCP:1087 -sTCP:LISTEN || true"
|
||||
run "DNS State" scutil --dns
|
||||
run "IPv4 Routes" netstat -rn -f inet
|
||||
run "IPv6 Routes" netstat -rn -f inet6
|
||||
run "Interfaces" ifconfig
|
||||
run_zsh "Burrow And VPN Processes" "ps aux | egrep -i 'Burrow|BurrowNetworkExtension|nesessionmanager|neagent|Surge|Tailscale|Clash|mihomo|sing-box' | egrep -v 'egrep|burrow-doctor'"
|
||||
run_zsh "Burrow Socket Owners" "lsof -U | egrep 'burrow.*sock|Burrow|BurrowNetworkExtension|NetworkExtension' || true"
|
||||
run "App Group Directory" ls -la "$group_dir"
|
||||
|
||||
section "Selected Proxy Server Connectivity"
|
||||
if [[ -f "$db_path" ]]; then
|
||||
run_python - "$db_path" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
db_path = sys.argv[1]
|
||||
|
||||
|
||||
def choose_node(payload: dict) -> dict | None:
|
||||
nodes = payload.get("nodes") or []
|
||||
selected_name = payload.get("selected_name")
|
||||
selected_ordinal = payload.get("selected_ordinal")
|
||||
for node in nodes:
|
||||
if selected_name is not None and node.get("name") == selected_name:
|
||||
return node
|
||||
if selected_ordinal is not None and node.get("ordinal") == selected_ordinal:
|
||||
return node
|
||||
return nodes[0] if nodes else None
|
||||
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(
|
||||
"select payload from network where type = 'ProxySubscription' order by idx limit 1"
|
||||
).fetchone()
|
||||
if row is None:
|
||||
print("No ProxySubscription network in database.")
|
||||
raise SystemExit
|
||||
|
||||
payload_blob = row["payload"]
|
||||
payload_text = payload_blob.decode("utf-8", errors="replace") if isinstance(payload_blob, bytes) else payload_blob
|
||||
payload = json.loads(payload_text)
|
||||
node = choose_node(payload)
|
||||
if not node:
|
||||
print("ProxySubscription has no nodes.")
|
||||
raise SystemExit
|
||||
|
||||
server = node.get("server")
|
||||
port = int(node.get("port"))
|
||||
print(f"selected_proxy_server={server}:{port}")
|
||||
|
||||
addresses = []
|
||||
try:
|
||||
for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(server, port, type=socket.SOCK_STREAM):
|
||||
host, resolved_port = sockaddr[:2]
|
||||
key = (host, resolved_port)
|
||||
if key not in addresses:
|
||||
addresses.append(key)
|
||||
except Exception as exc:
|
||||
print(f"resolve_error={type(exc).__name__}: {exc}")
|
||||
raise SystemExit
|
||||
|
||||
print("resolved_addresses=" + ",".join(f"{host}:{resolved_port}" for host, resolved_port in addresses))
|
||||
|
||||
for host, _ in addresses:
|
||||
print(f"\n$ route -n get {host}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["route", "-n", "get", host],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=5,
|
||||
)
|
||||
print(result.stdout.rstrip())
|
||||
print(f"[exit={result.returncode}]")
|
||||
except Exception as exc:
|
||||
print(f"route_probe_error={type(exc).__name__}: {exc}")
|
||||
|
||||
for host, resolved_port in addresses:
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with socket.create_connection((host, resolved_port), timeout=5):
|
||||
elapsed_ms = (time.monotonic() - started) * 1000
|
||||
print(f"tcp_connect_ok address={host}:{resolved_port} elapsed_ms={elapsed_ms:.1f}")
|
||||
except Exception as exc:
|
||||
elapsed_ms = (time.monotonic() - started) * 1000
|
||||
print(f"tcp_connect_error address={host}:{resolved_port} elapsed_ms={elapsed_ms:.1f} error={type(exc).__name__}: {exc}")
|
||||
PY
|
||||
else
|
||||
echo "DB not found: $db_path"
|
||||
fi
|
||||
|
||||
run_zsh "Route Probes" "for host in 1.1.1.1 8.8.8.8 google.com; do echo; echo \"\$ route -n get \$host\"; route -n get \"\$host\" 2>&1 || true; done"
|
||||
|
||||
run_zsh "DNS Probes" "if command -v dig >/dev/null 2>&1; then dig +time=3 +tries=1 google.com A; echo; dig +time=3 +tries=1 @1.1.1.1 google.com A; else dscacheutil -q host -a name google.com; fi"
|
||||
|
||||
run_zsh "Scoped Physical DNS Probes" "if command -v dig >/dev/null 2>&1; then servers=\$(scutil --dns | awk 'BEGIN { scoped=0; current=\"\" } /^DNS configuration \\(for scoped queries\\)/ { scoped=1; next } scoped && /^resolver #/ { current=\"\"; next } scoped && /nameserver\\[[0-9]+\\]/ { current=\$3 } scoped && /if_index/ && \$0 !~ /utun/ && current != \"\" { print current; current=\"\" }'); if [[ -z \"\$servers\" ]]; then echo 'No scoped non-utun DNS servers found.'; else for server in \$servers; do echo; echo \"\$ dig +time=3 +tries=1 @\$server google.com A\"; dig +time=3 +tries=1 @\"\$server\" google.com A; done; fi; else echo 'dig not found'; fi"
|
||||
|
||||
run_zsh "TCP Probes" "for target in '1.1.1.1 53' '1.1.1.1 443' 'google.com 443'; do set -- \$target; echo; echo \"\$ nc -vz -w 5 \$1 \$2\"; nc -vz -w 5 \"\$1\" \"\$2\" 2>&1 || true; done"
|
||||
|
||||
run_zsh "HTTP Probes" "for url in 'http://www.gstatic.com/generate_204' 'https://www.google.com/generate_204' 'https://www.cloudflare.com/cdn-cgi/trace'; do echo; echo \"\$ curl -4 -I --connect-timeout 5 --max-time 10 \$url\"; curl -4 -I --connect-timeout 5 --max-time 10 \"\$url\" 2>&1 || true; done"
|
||||
|
||||
run_zsh "Burrow Controlled Proxy Self-Test" "if [[ -x '$repo_root/Scripts/burrow-proxy-selftest' ]]; then '$repo_root/Scripts/burrow-proxy-selftest'; else echo 'Scripts/burrow-proxy-selftest not found or not executable'; fi"
|
||||
|
||||
run_zsh "Burrow Direct Provider Proxy Ladder" "if [[ -x '$repo_root/Scripts/burrow-proxy-ladder' ]]; then '$repo_root/Scripts/burrow-proxy-ladder' --db '$db_path' --resolve-server doh-all --target google.com:80 --http-host google.com --skip-udp --timeout 8; else echo 'Scripts/burrow-proxy-ladder not found or not executable'; fi"
|
||||
|
||||
run_zsh "Burrow Daemon Packet Proxy Probe" "if [[ -x '$repo_root/target/debug/burrow' ]]; then BURROW_SOCKET_PATH=\"\${BURROW_SOCKET_PATH:-burrow.sock}\" '$repo_root/target/debug/burrow' proxy-tcp-probe --dns-name google.com --remote 1.1.1.1:80 --timeout-ms 12000; else echo 'target/debug/burrow not found; run cargo build -p burrow first'; fi"
|
||||
|
||||
run_zsh "Burrow Daemon HTTPS Packet Proxy Probe" "if [[ -x '$repo_root/target/debug/burrow' ]]; then BURROW_SOCKET_PATH=\"\${BURROW_SOCKET_PATH:-burrow.sock}\" '$repo_root/target/debug/burrow' proxy-https-probe --dns-name news.ycombinator.com --remote 1.1.1.1:443 --timeout-ms 20000; else echo 'target/debug/burrow not found; run cargo build -p burrow first'; fi"
|
||||
|
||||
section "Proxy Subscription Endpoint Status"
|
||||
if [[ -f "$db_path" ]]; then
|
||||
run_python - "$db_path" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
db_path = sys.argv[1]
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(
|
||||
"select payload from network where type = 'ProxySubscription' order by idx limit 1"
|
||||
).fetchone()
|
||||
if row is None:
|
||||
print("No ProxySubscription network in database.")
|
||||
raise SystemExit
|
||||
|
||||
payload_blob = row["payload"]
|
||||
payload_text = payload_blob.decode("utf-8", errors="replace") if isinstance(payload_blob, bytes) else payload_blob
|
||||
payload = json.loads(payload_text)
|
||||
url = (payload.get("source") or {}).get("url")
|
||||
if not url:
|
||||
print("ProxySubscription has no source URL.")
|
||||
raise SystemExit
|
||||
|
||||
print("source_url_redacted=yes")
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "mihomo/1.18.3"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=20) as response:
|
||||
body = response.read(1024 * 1024)
|
||||
print(f"status={response.status}")
|
||||
for key, value in response.headers.items():
|
||||
lower = key.lower()
|
||||
if (
|
||||
lower in {"subscription-userinfo", "profile-update-interval", "profile-title", "content-type"}
|
||||
or "expire" in lower
|
||||
or "subscription" in lower
|
||||
):
|
||||
print(f"header_{key}={value}")
|
||||
print(f"body_bytes={len(body)}")
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"status={exc.code}")
|
||||
print(f"reason={exc.reason}")
|
||||
for key, value in exc.headers.items():
|
||||
lower = key.lower()
|
||||
if (
|
||||
lower in {"subscription-userinfo", "profile-update-interval", "profile-title", "content-type"}
|
||||
or "expire" in lower
|
||||
or "subscription" in lower
|
||||
):
|
||||
print(f"header_{key}={value}")
|
||||
body = exc.read(1000).decode("utf-8", errors="replace").strip()
|
||||
if body:
|
||||
print(f"body_prefix={body[:200]}")
|
||||
except Exception as exc:
|
||||
print(f"fetch_error={type(exc).__name__}: {exc}")
|
||||
PY
|
||||
else
|
||||
echo "DB not found: $db_path"
|
||||
fi
|
||||
|
||||
section "Burrow Runtime Log File"
|
||||
if [[ -f "$runtime_log" ]]; then
|
||||
cmd tail -400 "$runtime_log"
|
||||
printf '\n'
|
||||
tail -400 "$runtime_log" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?"
|
||||
else
|
||||
echo "Runtime log not found: $runtime_log"
|
||||
fi
|
||||
|
||||
section "Recent Burrow NetworkExtension Logs"
|
||||
cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"nesessionmanager\" AND eventMessage CONTAINS[c] \"Burrow\""
|
||||
printf '\n'
|
||||
{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"nesessionmanager\" AND eventMessage CONTAINS[c] \"Burrow\"" 2>&1 \
|
||||
|| printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; }
|
||||
|
||||
section "Recent Burrow Extension Logs"
|
||||
cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"$extension_process\" OR eventMessage CONTAINS[c] \"$bundle_id.network\""
|
||||
printf '\n'
|
||||
{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"$extension_process\" OR eventMessage CONTAINS[c] \"$bundle_id.network\"" 2>&1 \
|
||||
|| printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; }
|
||||
|
||||
section "Recent Burrow Errors"
|
||||
cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "((process CONTAINS[c] \"Burrow\") OR (eventMessage CONTAINS[c] \"$bundle_id\") OR (eventMessage CONTAINS[c] \"Trojan\") OR (eventMessage CONTAINS[c] \"proxy\")) AND (messageType == error OR messageType == fault OR eventMessage CONTAINS[c] \"failed\" OR eventMessage CONTAINS[c] \"error\" OR eventMessage CONTAINS[c] \"panic\")"
|
||||
printf '\n'
|
||||
{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "((process CONTAINS[c] \"Burrow\") OR (eventMessage CONTAINS[c] \"$bundle_id\") OR (eventMessage CONTAINS[c] \"Trojan\") OR (eventMessage CONTAINS[c] \"proxy\")) AND (messageType == error OR messageType == fault OR eventMessage CONTAINS[c] \"failed\" OR eventMessage CONTAINS[c] \"error\" OR eventMessage CONTAINS[c] \"panic\")" 2>&1 \
|
||||
|| printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue