kitsunping

Daemon - Kitsunping

Description

The daemon is a background service that continuously monitors network connectivity and quality. Kitsunping uses it to evaluate latency, packet loss, and stability, then emit events and trigger profile application when conditions change.

Features

Configuration

When the module is installed the user can select Static (Stable) or Dynamic (Adaptive) mode for Kitsunping Daemon from the Kitsunping App settings.

Usage

The module works autonomously once installed and configured. Users can monitor behavior through the Kitsunping logs and cache state files.

Execution summary

Below is a diagram showing the daemon execution flow.

After installing Kitsunping.zip and rebooting, the daemon starts from Android late-service (installer/service.sh) without additional manual steps.

During normal use, the daemon checks network state on the configured interval and can also react immediately on event-based transitions.

When conditions are met, the daemon emits events and spawns the Executor. The executor applies the target profile and decides if a calibration should run (cooldown + low-score streak).

When calibration is ongoing, the executor/calibration pipeline writes cache/calibrate.state and cache/calibrate.ts. These files can be polled by external clients (including the app) to display calibration status.

To avoid interference between daemon probing and calibration pings, the daemon skips Wi‑Fi probes/penalties while cache/calibrate.state=running.

Before applying BEST values from calibration via resetprop (without requiring reboot), the executor writes the currently applied profile to policy.current. The daemon (or an external policy selector) may write the desired profile to policy.request (informational) and trigger the executor.

Executor reads policy.target (target profile) and compares it to policy.current (last applied). If they differ, the executor applies the profile and updates policy.current, plus calibrate.* state to avoid repeated runs.

When a profile change is required, the policy path resolves and executes the corresponding *_profile.sh script in net_profiles/.


Timing and cadence

This section documents the time-related behavior of the daemon/executor and how often calibration can run.

Daemon loop

Debounce vs interval coupling (practical)

Executor / calibration cadence

Calibration is not a periodic timer. It only runs when the executor is triggered (typically by an event or profile change) and the gating conditions allow it.

State flow and lock

Heavy-activity race prevention model

This model combines signal + mutex to avoid overlapping expensive operations:

Simple explanation:

Normal flow

  1. Daemon enters a heavy router window (experimental parsing/cache path).
  2. Daemon acquires cache/heavy_activity.lock.
  3. Daemon increments kitsunping.heavy_load.
  4. Daemon executes heavy work.
  5. Daemon decrements kitsunping.heavy_load.
  6. Daemon releases cache/heavy_activity.lock.
  7. Executor, when triggered, checks both:
    • kitsunping.heavy_load <= HEAVY_LOAD_MAX_FOR_CALIBRATE (default max = 0), and
    • lock availability.
  8. If both pass, calibration continues normally.

Explicit contention case (lock in action)

Example: daemon starts heavy router sync at the same moment executor wants to calibrate.

  1. Daemon acquires cache/heavy_activity.lock first and sets kitsunping.heavy_load=1.
  2. Executor reaches calibrate gate:
    • sees heavy_load=1 and marks calibration as postponed, or
    • if counter is stale but lock is busy, lock-acquire fails and calibration is still postponed.
  3. Executor does not run calibrate.sh; it updates calibrate.state=postponed and calibrate.ts.
  4. Daemon finishes heavy window, decrements counter to 0, releases lock.
  5. On next eligible executor trigger, calibration can run.

This dual guard prevents race conditions even if one signal (counter or lock) is temporarily inconsistent.

Starvation prevention (priority by time/retries)

To avoid indefinite calibration postponement under frequent heavy activity, executor escalates to priority mode after thresholds:

Force-mode behavior:

  1. Executor detects starvation (count or age threshold hit).
  2. Executor sets kitsunping.calibration.priority=1.
  3. Daemon heavy router cycle checks priority and yields (skips heavy section while priority is active).
  4. Executor retries lock acquisition with bounded wait (CALIBRATE_FORCE_LOCK_WAIT_SEC).
  5. On success, calibration runs and postpone tracker resets; on failure, it remains postponed and retries on next trigger.
  6. Executor clears kitsunping.calibration.priority at the end of the run.

Manual test: stale lock/counter recovery

Use this quick sequence from adb shell to validate both protections:

  1. Simulate stale state (as if daemon died after increment/lock):
    • mkdir -p /data/adb/modules/Kitsunping/cache/heavy_activity.lock
    • setprop kitsunping.heavy_load 1
  2. Trigger executor once (normal event path or manual run).
  3. Expected runtime behavior:
    • If lock is truly busy, calibration is postponed.
    • If lock is stale/free, executor self-heals (heavy_load -> 0) and continues.
  4. Reboot device.
  5. Expected boot behavior (service.sh self-heal):
    • cache/heavy_activity.lock is removed.
    • kitsunping.heavy_load is reset to 0.

Verification hints:

Policy event payload

SELinux and ping capability checks

flowchart TD
    A[System boot] --> B[Magisk stage: post-fs-data.sh]
    B --> C[Magisk stage: service.sh]
    C --> D{sys.boot_completed == 1?}
    D -->|wait| D
    D -->|yes| E[Apply base network tweaks<br/>sysctl + defaults]
    E --> F[Start Kitsunping daemon<br/>addon/daemon/daemon.sh]

    F --> G[Init logs + cache + pidfile]
    G --> H[Detect binaries<br/>ip/ping/jq/bc/resetprop]
    H --> I[Main loop<br/>every INTERVAL]

    I --> J[Read iface + Wi-Fi state]
    I --> K[Read mobile state]
    J --> L[Compute Wi-Fi score<br/>link/ip/egress + RSSI + probe]
    K --> M[Compute mobile score<br/>link/ip/egress]
    M --> MM[If mobile egress: poll signal<br/>write signal_quality.json]
    L --> N[Write daemon.state]
    M --> N

    N --> O{State changed?}
    O -->|iface wifi signal| P[Write event files<br/>daemon.last + event.last.json]
    P --> Q[Spawn executor async<br/>policy executor.sh]
    O -->|no| I

    %% Profile decision:
    %% - Daemon emits PROFILE_CHANGED when desired profile changes
    %% - Executor is the single-writer of policy.target/policy.current
    Q --> R{EVENT_NAME == PROFILE_CHANGED?}
    R -->|yes| S[Write policy.target using atomic]
    R -->|no| T

    T --> TT{policy.target exists?}
    TT -->|no| I
    TT -->|yes| U[Compare policy.target vs policy.current]
    U --> V{Different?}
    V -->|no| I
    V -->|yes| W[Apply profile if available]

    W --> X{Calibration gating<br/>cooldown and low score streak}
    X -->|run| Y[Run calibrate.sh<br/>logs/results.env]
    Y --> Z[Apply BEST values<br/>via resetprop]
    Z --> AA[Update state files<br/>policy.current + calibrate.*]
    AA --> AB[Write policy.event.json]
    AB --> I

    subgraph State_Files[State files cache/]
        SF1[daemon.state]
        SF2[daemon.pid]
        SF3[daemon.last]
        SF4[event.last.json]
        SF5[signal_quality.json]
        SF6[policy.request]
        SF7[policy.target]
        SF8[policy.current]
        SF9[policy.event.json]
        SF10[calibrate.state]
        SF11[calibrate.ts]
        SF12[calibrate.streak]
    end

    subgraph Logs[Logs logs/]
        LG1[daemon.log / daemon.err]
        LG2[policy.log]
        LG3[services.log]
        LG4[results.env]
    end

    N -.-> SF1
    F -.-> SF2
    P -.-> SF3
    P -.-> SF4
    MM -.-> SF5
    S -.-> SF7
    AA -.-> SF8
    AB -.-> SF9
    AA -.-> SF10
    AA -.-> SF11
    AA -.-> SF12

Daemon / Kitsunping Properties

Debugging and Performance Tuning

Router / OpenWrt

When router.experimental=0, daemon skips router parsing/signature pipeline and behaves as stable baseline.

Wi‑Fi decision

Calibration cache (Net_Calibrate)

Scoring weights