Safety
BHTune's MRFT test switches a real loop to manual and strokes its MV — the same as the legacy tool, but with one important difference: the legacy tool assumed an operator was always watching and could hit Stop. BHTune adds real guardrails for scheduled and scripted tunes that run with nobody present. This page explains exactly what those guardrails do, since "what happens if I press Ctrl+C" and "what happens if the network hiccups mid-test" both matter before you point BHTune at a real process.
Cancellation
Pressing Ctrl+C during a bhtune tune/bhtune simulate run (or clicking Cancel on the web
GUI's run detail screen, which triggers the same code path) requests cancellation and
best-effort restoration. It does not guarantee that the controller has been restored:
- First Ctrl+C stops polling immediately and starts the restore (see
Restoration below). It works no matter when it's pressed — including mid-read
or mid-write to a stalled driver, not just while idle between poll ticks. Every in-flight
driver call is bounded by the global
[tuning].op_timeout_secssetting (default 30s), so a stalled OPC DA read or write is abandoned rather than waited on forever, which is what makes cancellation reliable even against a wedged gateway or a black-holed network. - Second Ctrl+C, pressed while the restore itself is still running, forces an immediate hard
stop instead of waiting any longer for the restore to finish. BHTune prints exactly which MV
tag it was restoring and what value it was last written to, so you can put it back by hand.
This exits with a distinct code (
6, see the exit code table below) rather than the normal abort code, because "aborted and restored" and "aborted, restore abandoned — go check the loop" are different situations for whoever (or whatever scheduler) is watching the exit code. [tuning].timeout_secs(default 3600) is the overall wall-clock budget for the whole test. It fires the same restore path as Ctrl+C — including working correctly mid-hung-read — and is meant as the backstop for scheduled/unattended runs where nobody is present to press Ctrl+C at all.[tuning].restore_timeout_secs(default 30) is the initial budget for the restore step, independent of[tuning].timeout_secs— a restore triggered by a timeout doesn't inherit an already-expired budget. If the authoritative MV restore write is accepted near that deadline, BHTune extends the effective deadline as needed to preserve the complete four-second MV confirmation window; the remaining restore steps use that same effective deadline.
These five values are global installation settings in the browser's Configuration page or
the [tuning] section of bhtune.toml; they apply to future runs only. Try it yourself: set a
long [tuning].poll_interval_ms, start a run, and press Ctrl+C while it's waiting between ticks
(works immediately); then point it at an unreachable --bridge-host and press Ctrl+C — it
should abort and report within [tuning].op_timeout_secs, not hang.
Global tune timing settings
The [tuning] section controls the operational timing and safety limits shared by all new tunes:
| Setting | Default | Purpose |
|---|---|---|
mrft_delay_secs | 0 s | Pre/post-test recording padding |
poll_interval_ms | 800 ms | Delay between driver polls |
timeout_secs | 3600 s | Whole-run wall-clock limit |
op_timeout_secs | 30 s | Limit for one driver read or write |
restore_timeout_secs | 30 s | Initial restore budget; an accepted MV restore may extend it for confirmation |
Values must be valid before a tune can touch the loop. mrft_delay_secs accepts 0 through
3600; the other settings must be positive whole numbers. OPC DA preparation additionally
requires restore_timeout_secs of at least four seconds because accepted MV commands have a
four-second actuation-confirmation window. Simulator runs may use a shorter positive restore
timeout. Configuration changes affect future tune preparations only; an already-prepared or
running tune keeps its captured values.
Public Demo mode
Demo mode is a separate, simulator-only server surface. Its 200 ms poll interval, 30-second
run timeout, request limits, quotas, session lifetime, and history caps are fixed
application-owned safeguards. The optional [demo] configuration table can declare those
values for deployment validation, but it cannot widen or otherwise override them. Demo runs
use the stable Simulator demo identity and never connect to OPC DA or write PID constants.
See the public simulator demo guide for the complete boundary and
self-hosting requirements.
Timing and host responsiveness
Live OPC DA runs measure MRFT time with a monotonic clock paired to the run's UTC start timestamp. The timestamps stored in history remain ordinary UTC values, but their progression comes from monotonic elapsed time. An NTP correction or manual system-clock adjustment after the run starts therefore cannot shorten, lengthen, reverse, or skip an apparent relay period.
Real delays are not hidden. If the operating system schedules BHTune late, the gateway responds slowly, or an OPC read/write takes longer than expected, that elapsed time remains part of the sample and switch timeline. The polling loop delays its next schedule instead of issuing a burst of catch-up reads or writes against a live controller.
BHTune is not a hard-real-time controller and cannot guarantee identical live samples on an overloaded host. Keep the BHTune host and OPC DA gateway responsive, avoid competing heavy work during a tune, and choose a poll interval comfortably shorter than the loop's expected oscillation period. The whole-run and per-operation safety timeouts remain independent monotonic timers.
Each run with at least one successful PV poll stores a timing snapshot in history. The CLI's
bhtune history show <run> output, the run-detail API, and structured logs retain the requested
interval, observed sample-gap count, mean and maximum sample gap, measured oscillation period
when the test completed, and approximate samples per period. The normal web run-detail page
intentionally omits these low-level diagnostics so it can focus on actionable run and safety
information. A live run is flagged in structured logs when an adjacent sample gap is at least
twice the requested interval, because that objectively means at least one complete polling
opportunity was missed. This is a warning, not a validity verdict: it does not abort the run,
change its calculated constants, or prevent an engineer from applying them.
The recorded timing data also retains sampling adequacy for CLI history and the run-detail API.
adequate means at least six observed samples per measured oscillation
period; marginal means fewer than six; and not_assessed means no usable finite period was
available. This is an advisory signal, not an automatic rejection: a valid result with marginal
sampling remains writable, but should be reviewed against the trend and the recorded timing data
before it is applied. The normal web page does not display this advisory as a tuning-quality
badge; invalid results and actual quality, actuation, and restoration failures remain visible.
Live Auto-release settling
When a live OPC DA tune starts with the loop in the template-defined Auto value and the MRFT completes normally, BHTune does not release the loop to Auto immediately. It first restores and verifies the original MV, keeps the loop in Manual for one-third of the measured oscillation period, and polls the PV at the configured interval during that hold. Those PV samples are persisted as ordinary run samples, so they appear in the trend, history, API responses, and exports. The MRFT state remains frozen during the hold: no additional relay decisions or MV commands are issued.
When the initial raw mode matches the template's Auto value and a setpoint tag is configured,
BHTune reads the original setpoint while the loop is still in Auto, before any mode or MV write.
It persists that value with the run's initial readings before beginning the mode transition.
The captured value is the restore target even if BHTune did not change the setpoint during the
test. A non-Auto starting mode (for example, MAN or CAS) does not cause a setpoint read or
setpoint write-back; the original controller mode is handled separately according to the
template's revert_mode setting.
After the hold, the final restore sequence writes the template-defined Auto value first, then attempts to write back the captured setpoint, followed by the original mode-attribute value. The setpoint is therefore not restored before the controller is switched to Auto. These steps are best-effort and are not atomic: if the Auto-mode write succeeds but the later setpoint or mode-attribute write fails, or the restore is interrupted by its deadline or a second Ctrl+C, the controller may already be in Auto with some original values unrestored. The run reports an incomplete restore; check the actual mode, setpoint, and mode attribute at the DCS/PLC before another tune.
If the settling hold itself fails before final mode restoration begins, BHTune skips its Auto-release write and leaves the mode as it was at that point (normally Manual). It still attempts the remaining applicable restore steps, subject to the remaining restore-time budget, and reports an incomplete restore for operator follow-up. Simulator/Demo runs, non-Auto starts, and runs without a valid measured period do not use the settling hold. The hold shares the configured restore-time budget and never extends it.
The timing snapshot includes successful PV-read, MV-write, MV-verification-read, sample-persistence, and total-tick-work latency summaries in addition to sample gaps. When a pending relay is checked by the normal batched PV/MV poll, the same underlying OPC operation may contribute to both the PV-read and MV-verification categories; those categories describe overlapping evidence and must not be added together as independent network durations. Failed, cancelled, and timed-out operations are excluded from these successful-latency measurements.
Input validation
Every number that reaches the tuning engine is validated before any live I/O happens: relay
amplitude, cycle counts (a zero cycle count is rejected outright rather than reaching the engine
and panicking mid-test, which is what the earliest builds did), PV/MV ranges (must be finite and
correctly ordered — a NaN or an inverted range is rejected, not silently propagated into a PID
write), and the initial MV must fall inside the validated MV range. Command-line flags reject
non-finite/out-of-range input immediately with a clear message; anything read from the driver
(a real DCS/PLC's current ranges, for instance) is validated again right after being read, before
the loop is ever switched to manual. An effective relay step below the minimum that can be
distinguished safely at f32 precision is rejected at this same pre-mutation boundary.
Read-only tune preflight
bhtune check accepts the same tune inputs as bhtune tune and checks them before an MRFT is
started. For OPC DA it performs bounded gateway discovery and reads every derived tag once,
including PID constant tags. It reports values and OPC quality, validates template mode values,
and checks the initial PV/MV state and requested write-back readiness.
Full mode offers the same check from the New Tune page's Check readiness action. The
browser sends the same tune-request fields to POST /api/runs/preflight and displays the
overall pass/warn/fail result, individual checks, and tag reads. The route uses a read-only
database connection and read-only driver; it does not create run history, start a tune, or
write controller values. Demo mode does not mount the route or show the action.
The CLI command does not create or update run history, run a tune, change controller values, or probe write permissions. It uses a read-only database connection when saved templates are needed and a driver wrapper that rejects writes. When a template defines a mode-attribute program value, the check reads that value as another write-back prerequisite but never changes it. A passing write-back check confirms only that the P/I/D tags are configured and readable as numeric values and any template-specific prerequisites are met; it cannot prove a future write will be accepted or that an accepted MV write will move the plant. The GUI report has the same limitation. Follow the live-plant procedures below before running a tune.
--output json emits the full report. Exit code 0 means the checks passed, 1 means the
preflight could not run, and 8 means a check failed or --strict treated a warning as a
failure. Without --strict, warnings are reported but do not prevent a successful exit.
Invalid calculated results
Every response-level result is checked before it is stored as usable tuning data. A non-positive
or non-finite PV amplitude or period, or any non-finite intermediate or template-converted PID
value, is stored as Invalid with an explicit diagnostic reason and without numeric tuning
values. Invalid calculated-result rows cannot be selected for PID write-back in the CLI or web
GUI. This backstop remains important even when sampling is adequate, because a degenerate
measurement can arise from a future algorithm or data-path defect.
Template precision is a separate write-readiness check. An otherwise valid result remains unwritable if rounding an active P/I/D term to the template's final units makes it zero or non-finite. The CLI and browser report the reason before PID I/O; raw calculations and their validity remain unchanged. Increase the template precision and start a new run rather than overriding the recorded run's policy.
MV actuation verification
Every accepted OPC DA relay write is read back before a later relay command can replace it. While that command is pending, the normal poll requests PV and MV in one deduplicated batch and evaluates the MV observation before the PV sample is persisted or the MRFT engine advances. This makes the normal poll the primary verification path without weakening the safety rule. If normal polling does not provide usable evidence before the deadline, a separately bounded MV-only read remains available as a fallback.
The first check occurs at the earlier of the MRFT noise-protection boundary and four seconds after write acceptance. An early mismatch remains pending and is retried; a mismatch at four seconds, or when the engine genuinely needs the next relay command, aborts the run without writing the replacement.
The absolute tolerance combines the f32 precision floor with 0.1% of the configured MV span.
For relay commands it is capped at 25% of the actual step, preventing a wide range from making a
small command appear confirmed accidentally. Restore confirmation uses the same precision/span
tolerance without the relay cap. The final MRFT snapback hands responsibility to the
authoritative restore write, so BHTune does not wait twice for the same original-MV target.
The restore readback is attempted immediately; only a mismatch is retried.
A restore_timeout_secs value below four seconds is rejected during OPC DA preparation, before
a live loop is mutated. This is the initial budget: when an authoritative MV restore write is
accepted close to its end, the effective deadline extends to at least four seconds after that
acceptance so the required confirmation window cannot be cut short. Remaining mode and setpoint
restore steps share the extended deadline.
An MV read that starts before the deadline but returns after it is still treated as late and does
not confirm the command. The fresh read started at the deadline is independently capped at one
second, rather than inheriting the full per-operation timeout, so a stalled read cannot hold the
tune open indefinitely.
The MV observation is evidence about physical actuation, not a second trend point: it does not
advance MRFT time or add a separate trend/export sample. A batched PV/MV request does, however,
record the successful operation in both the PV-read and MV-verification latency categories;
those categories overlap because they describe one request. bhtune history show <run> records
each accepted command, observation, tolerance, deadline, and final status.
Gateway compatibility
Before an OPC DA tune switches a loop to manual, and before a PID write or revert touches the
loop, BHTune asks the opcda-bridge gateway which protocol versions it supports. The result is
stored with the run and shown by bhtune history show, bhtune opc gateway-info, and the run
detail page.
- Full means the gateway covers every protocol version this BHTune build requires. The operation proceeds with no compatibility warning.
- Partial means the core protocol overlaps, but an optional feature such as namespace browse or indexed search does not. The tune, write, or revert proceeds. Affected inspection features are degraded, and BHTune logs a warning.
- Unknown means the gateway did not report a protocol range. Live operations proceed with a warning. Discovery, browse, and read stay available and report the unverified result instead of failing the request.
- Incompatible means the core protocol ranges do not overlap. BHTune refuses the tune, write,
or revert before changing the loop and tells you to upgrade the gateway or BHTune so the core
ranges overlap. A refused tune is a setup failure (exit code
1) and does not create a running run. The web GUI returns the same refusal as an error and writes nothing.
Read-only OPC discovery, browse, and tag reads are not refused for an incompatible gateway. They return the compatibility result so you can see what the gateway cannot do. See OPC gateway compatibility for the checks and where the snapshot is shown.
OPC quality
Every OPC DA read reports a quality alongside its value (Good/Uncertain/Bad). BHTune
always accepts Good, accepts Uncertain by default, and never accepts Bad for tuning-critical
operations:
- Before the loop is touched (initial PV/MV/range/mode/direction reads): a
Bador policy-rejectedUncertainreading is a hard failure. Nothing has been mutated yet, so this is a clean refusal. - During the test (every polled PV sample): a
Bador policy-rejectedUncertainsample aborts the run and attempts to restore the loop. A held or stale PV during a relay half-cycle would corrupt the exact period measurement the test depends on — silently tolerating it is worse than aborting loudly. The triggering sample is still recorded (with its real quality) before the abort. - During MV actuation verification: an unacceptable readback is recorded but does not immediately prove failure. Confirmation remains pending and is retried until the four-second deadline; if the engine needs the next relay command first, the run aborts without issuing that replacement.
- During write-back confirmation: a
Bador policy-rejectedUncertainreadback is treated as an unconfirmed write, which triggers rollback (see PID write-back below). - When selecting a tag in the web browser: BHTune re-reads the exact item selected in the
tree before applying the template's PV suffix.
Goodquality proceeds normally;UncertainorBadquality requires an explicit choice to select another tag or proceed anyway. This only accepts the item into the form; tune execution still enforces the quality rules above.
Bad quality is never accepted under any setting. Sites whose gateway reports Uncertain as a
matter of course can leave the default allow_uncertain_quality = true, or disable that global
policy on the Config page / in bhtune.toml when uncertain readings must be rejected —
enabled by default, logged loudly every time it changes the outcome, and recorded on the run so
history shows which runs executed under relaxed rules.
Restoration
BHTune guarantees a best-effort restore on every exit path — successful completion, an error partway through, Ctrl+C, or a timeout — not just the happy path. Before the first live write, BHTune captures and persists the initial mode and mode-attribute values and, only when the raw starting mode is the template-defined Auto value and a setpoint tag is configured, the initial setpoint. The mutation guard separately records which mode-transition writes were attempted. The original MV restore is always attempted; the other restore steps depend on the captured values, transition state, and template settings.
In the normal release-to-initial path, restore proceeds in this order: original MV, original controller mode when applicable, captured Auto-start setpoint when applicable, and original mode-attribute value when applicable. A keep-Manual path omits the controller-mode restore. Restore writes are attempted independently, so one failed step does not by itself prevent later steps from being tried; a restore deadline or second Ctrl+C can interrupt the sequence.
bhtune history show <run-id> (or the run detail screen) reports the restore outcome as
confirmed or incomplete, with the failed step details. For live OPC DA, the original MV
has a separate readback-confirmation step; controller-mode, setpoint, and mode-attribute
restoration record write results but do not have their own readback verification. An incomplete
restore does not guarantee that the controller is still in Manual: in the return-to-Auto path,
Auto is written before the setpoint and mode attribute, so one of those later steps can fail
after Auto has already been requested. Inspect the actual DCS/PLC values before another tune.
An incomplete restore exits with code 6, distinct from a normal abort.
Preparation records the run before writing its effective timing, quality policy, connection, and other provenance metadata. If one of those follow-up writes fails, BHTune immediately marks the row failed instead of leaving it permanently running. If the terminal update itself cannot be persisted, the row is removed as a fallback and the failure is logged.
Recovering an interrupted live tune
Full-mode startup checks for abandoned live-operation owners but never connects to a controller
or attempts a restore. Each live operation holds an OS-level database lock and a conditional
database claim. A heartbeat older than 30 seconds only makes an owner a candidate: if the
process is paused and still holds the lock, startup cannot take ownership and does not recover
the loop. Before changing orphan state, startup exports the run, owner, and mutation evidence
next to the database in <database-file>.recovery/.
Only an owned, still-running OPC tune with complete and consistent recorded connection,
resource identity, initial readings, template policy, timing, restore intent, and mutation
audit becomes explicitly eligible. An Auto-start run with a configured setpoint restore target
must have its initial setpoint recorded. Legacy rows and incomplete or mismatched evidence fail
closed. Review the exported evidence and run detail before an intentional live recovery:
bhtune restore-loop <run-id> --yes --output table
Use --output json for a machine-readable report. Optional --bridge-host and --server
arguments only verify the recorded connection; they cannot redirect recovery. BHTune reuses
the original run's connection and restore policy, confirms the MV under the normal quality and
four-second accepted-write deadline, and reports the outcome of every permitted restore step.
Exit 0 means the recovery and its audit were confirmed. Exit 6 means recovery was
incomplete or its required final audit was not persisted; inspect the per-step report and the
actual loop before taking further action. When MV confirmation fails, BHTune can intentionally
keep the loop in Manual. If a row is not eligible, do not bypass the guard: use the exported
evidence and the site's manual recovery procedure. Demo mode does not run this live recovery
sweep.
PID write-back
Omitting --write-pid does not make a live MRFT read-only: it still changes mode and commands
MV relay steps. Use bhtune check or Full mode's Check readiness for a read-only preflight.
The CLI requires --yes with --write-pid <level> on every tune path, including interactive use;
that flag confirms unattended PID write-back. The Web UI requires its explicit review/confirmation.
Completed OPC runs can also review/write results after the tune. BHTune:
- Reads and persists the current P/I/D values first, before writing anything. If this pre-read fails, nothing is written at all.
- Writes and verifies each constant individually (P, then I, then D), checking the readback against what was requested within a small tolerance — a DCS's own unit rounding means a just-written value isn't always bit-identical on readback, so exact equality would produce false failures.
- Rolls back only what was actually confirmed if any constant fails partway through — if P succeeds and I fails, only P is rolled back (D, never attempted, needs nothing; I, never confirmed, has nothing to put back).
- Writes D explicitly for PI controllers. A PI result writes its calculated integral value
and
D = 0.0, clearing any stale derivative action in the controller. A P-only result uses the template-specific integral-disabling sentinel; a full PID result writes its calculated derivative value. - Records every outcome, including which case applies: nothing written, everything written
and confirmed, a partial write successfully rolled back, or — the case that needs a human —
a partial write whose rollback itself failed. That last case prints a message pointing at
bhtune history revert <run-id>, which writes the persisted previous values back under the same pre-read/verify contract, so a write-back that turns out wrong can be undone later without anyone having written the old numbers down by hand.
New PID writes use the template policy snapshotted on the run: one decimal place for Yokogawa, three significant digits for the other built-ins. Reviews and writes agree. Inactive integral/derivative disable values remain exact. Pre-write values, actual readbacks, rollback, PID revert, and loop recovery are never quantized by that policy. See PID precision for custom-template settings.
Network exposure
bhtune-server binds 127.0.0.1 (localhost only) by default and ships with no
authentication — anyone who can reach the port can start, cancel, or configure a tune.
Binding to any other address (BHTUNE_BIND=0.0.0.0:8787 or a LAN IP) is an explicit choice you
make yourself; there is no installer-driven firewall rule or prompt that does this for you.
Until authentication ships (a planned, not yet available, feature), treat a non-loopback bind
the same way you'd treat any other unauthenticated service on your OT network: only do it on a
trusted, isolated network, and prefer console/remote-desktop access to the host running
bhtune-server over exposing it further.
Full mode provides CSRF protection for browser mutations without requiring per-host setup:
when no BHTUNE_ORIGIN or origin config value is present, the server accepts an Origin only
when its host and effective port match the request Host. Set an explicit origin when a reverse
proxy rewrites Host or when one external browser origin must be pinned. Requests without an
Origin remain available for CLI/curl compatibility. This policy does not authenticate users,
authorize operators, or make an unauthenticated non-loopback deployment safe to expose publicly.
The optional Windows-installer gateway has a different network contract. It runs as
LocalSystem, is unauthenticated, and listens on 0.0.0.0:7600 so BHTune and approved remote
clients can reach native OPC DA/DCOM on that host. Interactive installation displays this
warning before a clean install selects the component; silent clean installation requires
/INSTALL_GATEWAY=1. The installer never creates or modifies a Windows Firewall rule.
Selecting the component is therefore not authorization to expose TCP 7600 beyond a trusted OT
network.
The installer refuses to adopt an existing unowned OpcdaBridgeGateway registration, overwrite
unexpected content in its managed Program Files directory, or proceed while TCP 7600 has an
unexpected listener. Do not work around those checks by deleting services, terminating
processes by name, or changing ownership metadata. Resolve the existing deployment explicitly
and preserve its configuration/data first.
Packaging and database recovery boundaries
The Windows NSIS installer keeps exactly one verified rollback backup under
%ProgramData%\ByteHound\bhtune\ once an installer-managed database exists. A genuinely empty
clean install has no prior database to protect and therefore has no rollback backup. Automatic
database backup and rollback cover only the
installer-managed SQLite database named by the absolute top-level db setting in the preserved
configuration, including its -wal and -shm companions when present. The installer validates
that path before stopping the service; it refuses malformed, relative, ambiguous, or external
database paths by default.
When the optional gateway is installer-owned, both Windows services participate in one transaction. The installer stops them before replacement, snapshots the gateway executable and the complete gateway ProgramData subtree, validates the candidate service/listener/read-only smoke path, and restores both service definitions, data, and prior running/stopped states on failure. Gateway configuration, index database and SQLite sidecars, build metadata, logs, and rollback evidence remain inside this boundary even when BHTune uses an external database.
An operator who prepares an existing external database file and independently backs it up can
explicitly acknowledge that boundary with /CUSTOM_DB_BACKUP_CONFIRMED=1. A missing or
inaccessible external file, or a path that names a directory, fails closed before the installer
stops the service; the installer does not create external database parents or files. After the
preflight succeeds, the installer may replace binaries and service state, but the external
database remains operator-owned: the installer does not claim to have backed it up or to be able
to roll it back. A failed health/version validation, including validation of the restored service
after rollback, still requires checking that database and the service manually.
The NSIS-installed BHTune service runs as NT AUTHORITY\LocalService. An external database
therefore needs service-account access on both the existing database file and its parent
directory, including permission to create or update SQLite's -wal and -shm sidecars.
Elevated installer access is not evidence that the service can use an operator-owned path; the
installer leaves those ACLs unchanged and the startup health check is the definitive validation.
Grant only the minimum access required for the service, and independently back up the database
before acknowledging the override. The optional gateway runs separately as LocalSystem,
matching the upstream native OPC DA/DCOM service contract.
Linux packages preserve /etc/bhtune and /var/lib/bhtune across upgrades and removal; they do
not silently delete operator data. Docker deployments have the same boundary through the mounted
/var/lib/bhtune volume. For every packaging path, stop the service before copying or restoring a
SQLite database and keep the database, -wal, and -shm files together. Package installers do
not create firewall rules or widen BHTune's loopback bind. The optional Windows gateway retains
its explicit all-interface bind without adding a firewall rule. Windows uninstall uses a guarded
two-pass cleanup: ProgramData, gateway configuration/index/logs, installer recovery state, and
operator data remain in place, while ownership metadata is removed only after the fixed Program
Files tree has been independently verified absent.
The Debian, RPM, and Arch package units are package-managed variants of the same hardened
systemd service and use /usr/bin/bhtune-server; the manual archive unit is separate and uses
/usr/local/bin/bhtune-server. Installing any package reloads systemd metadata but does not
enable or start BHTune. Operators must explicitly run sudo systemctl enable --now bhtune-server after reviewing the configuration. Package upgrades preserve the previous
service state, and package removal preserves /etc/bhtune, /var/lib/bhtune, SQLite sidecars,
and logs rather than treating them as disposable payload files.
Debian package dependency metadata is intentionally adaptive: cargo-deb uses
depends = "$auto" and relies on dpkg-shlibdeps in a Debian-capable packaging environment.
An empty Depends: field from a host that lacks that tool is an incomplete packaging result, not
evidence that the runtime has no shared-library requirements. Do not install such an artifact
without rebuilding it in the supported Debian environment or independently verifying its
dependencies.
The Arch bhtune-bin package is generated from exact stable release inputs and is not a live
source build. Its publication workflow rejects prerelease and arbitrary refs, validates every
source checksum, builds as a non-root Arch user, and never enables or starts the service during
package installation. Until the first stable release and manual AUR publication exist, use the
workflow only for validation; do not present bhtune-bin as an available public package.
The Windows installer is not Authenticode-signed. SmartScreen may therefore warn about the publisher even when the file is intact. Verify the release checksum and, when available, the Sigstore bundle and GitHub provenance separately; those checks establish integrity and build provenance, not Windows publisher trust.
If an upgrade health check fails, leave the affected services stopped or in the
installer-reported rollback state and preserve the installer log, service definitions, health
response, gateway listener/process evidence, and database files. Do not repeatedly rerun an
upgrade against the same live database while the failure is unexplained. For an NSIS
installation, the installer reports rollback success only after the restored BHTune service
answers the expected health/version check and a previously running gateway passes its bounded
read-only smoke check; if either check fails, treat the rollback as incomplete, inspect the
preserved ProgramData rollback directory, and restore the
database plus -wal/-shm companions only while the service is stopped. For package or manual
archive installs, keep the prior verified package/archive and restore the binaries as a matched
pair, then start the service and verify /api/health before considering the recovery complete.
The operator runbook lists the evidence to collect and
the bounded recovery sequence.
The frontend development server is also unauthenticated and binds all local interfaces.
Use its loopback URL on a trusted development host; other hostnames require Vite's host allowance
and the exact browser origin. It proxies API requests to local bhtune-server; see
development setup.
Scripting and exit codes
--output json emits exactly one parseable JSON value on stdout, on every path — success,
abort, timeout, poor quality, restore-incomplete, or write-back failure — so a scheduler never
has to guard against stray prose interleaved with the object it's trying to parse. Exit codes
are equally specific:
| Code | Meaning |
|---|---|
0 | Completed successfully |
1 | Setup error (unknown template, bad flag combination, database/driver connection failure) |
2 | Aborted by Ctrl+C, restore confirmed |
3 | Test completed, but the requested PID write-back failed |
4 | [tuning].timeout_secs elapsed before the test finished |
5 | Bad or policy-rejected Uncertain quality aborted the run, with restore confirmed |
6 | The post-run restore could not be confirmed — check the loop by hand |
7 | An accepted OPC DA MV command could not be confirmed; restore was confirmed |
8 | A preflight check failed, or check --strict rejected a warning |
Exit code 6 takes precedence over 7 if the actuation failure is followed by an incomplete
restore.
Next steps
- OPC gateway compatibility — how BHTune decides whether a gateway is safe to mutate, and where that result is recorded.
- MRFT concepts — what the test is actually doing while these guardrails watch over it.
- CLI quickstart — see
--output jsonand the automation flags in context.