Mechanical, behaviour-preserving reformat produced by 'make cs-fix' under
the new build/phpcs.xml.dist ruleset: K&R braces, tab indentation, and the
other whitespace normalisations. No logic changes.
The update/rollback flow shells out to `sudo` (the Python updater), which
only works when the process is already root — in XC_VM's model the xc_vm
user has no sudoers entry. Run as a non-root user, the nested sudo prompts
for a password it can never read (no tty) and the updater silently never
launches, leaving the server stranded at status=5 (updating) with nothing
left to reset it.
Add a private assertRunAsRoot() check at the top of the update and
rollback cases, before any status change, plus a launcher pre-flight
(updater script + interpreter present) so status=5 is only set once a
launch is actually possible.
Replace the single global `update_channel` setting with independent
per-repository channels for MAIN (panel), BIN (compiled binaries) and
FANOUT (xc_fanout daemon). The GeoLite/ASN (UPDATE) and proxy (PROXY)
repos have no channel of their own and follow the MAIN channel.
- Add XcVm\Core\Updates\UpdateChannels resolver (main/bin/fanout/forRepo).
- Migration 016: add update_channel_main/bin/fanout, copy the previous
selection into all three (unstable -> beta), drop update_channel.
- Settings UI: one "Updates" section with three channel selectors.
- Route every GitHubReleases call site through the resolver.
- LbInstallFlow: resolve the BIN tag channel-aware (was always /latest),
so a BIN beta channel provisions LB nodes with pre-release binaries.
- XcvmCoreCommand: map the BIN channel to a git branch (stable -> main,
beta -> beta) with a fallback to the stable branch when beta is absent.
Adds a "safety net" rollback so a server can be downgraded to an earlier
release without a manual redeploy, mirroring the existing update flow.
- GitHubReleases: getVersionFile() fetches a specific version's asset (not
just latest); getPreviousVersions() lists the newest releases older than a
given version, each flagged beta (GitHub prerelease). getReleases() now
delegates to a shared getFilteredReleases() that keeps the prerelease flag.
- UpdateCommand: new `rollback <version>` case — validates the target is a
real, strictly-older release; on MAIN takes an automatic DB backup first
(aborting if it fails); downloads the exact version (main/lb_update asset),
verifies MD5, and hands off to the same python `update` applier.
- RootSignalsCronJob: dispatch the `rollback` signal to `console.php update
rollback <version>` (version regex-validated, escapeshellarg'd).
- api.php: `rollback_versions` (channel-aware list, optional per-server base
version) and server `sub=rollback` (validates version, writes the signal).
- servers.php: per-server "Rollback Version" action (dropdown item + button)
opening a modal to pick a version; beta builds show "(beta)". Works for
MAIN and each LB independently.
- docs: rollback sections in server-update.md and update-system.md.
The downgrade reuses the version-agnostic python applier, so binaries,
config and user data are preserved. Migrations are forward-only and are not
rolled back; the automatic MAIN backup is the recovery path.
Migrate SettingsManager::getAll()['key'] to SettingsManager::get('key') across
CLI commands and cron jobs. get() is used here (rather than the typed
accessors) because it returns the raw value unchanged, preserving the diverse
call sites verbatim: `=== null` checks, `?? ''`/`?? false` fallbacks, string
concatenation, numeric comparisons and ternaries. It still adds a default,
silencing PHP 8 "Undefined array key" warnings. Whole-array `$rSettings =
getAll()` grabs are left as-is.
Hoist the request-invariant redis_handler flag to method entry in
UsersCronJob/StreamsCronJob (loadCron/processDeletions), where it was read deep
inside nested per-connection loops.
Also fix two long-standing PHPStan baseline entries by initialising
$rRedisDelete and $rLiveKeys unconditionally in UsersCronJob (they were only
assigned inside `if ($rRedis)` but read under correlated guards) and drop the
now-stale baseline entries.
The xcvm_core PHP extension is mirrored into the binaries repo tree — decoupled
from the heavy per-distro runtime bundle — under bin/xcvm_core/ as per-OpenSSL-ABI
archives (openssl1.1 / openssl3) + version.json + SHA256SUMS. Nothing panel-side
kept it current, so nodes drifted onto whatever build their install archive shipped
— and critically the LB's build lacked XC_VM::config_set_redis, so an LB could not
point its Redis at the main server and every redis_handler connection failed with
LINE_CREATE_FAIL.
Add XcvmCoreCommand (console.php xcvm_core), modelled on FanoutBinaryCommand:
- resolves the latest version from version.json (raw, main branch);
- detects this host's OpenSSL ABI (libcrypto.so.3 -> openssl3, else openssl1.1;
both present -> prefer openssl3 with a load-test fallback to the other);
- downloads the matched archive, verifies SHA-256;
- installs the .so ATOMICALLY with a backup + fresh-php load-test + rollback, so a
wrong-ABI/broken extension never takes php-fpm down, then USR2-reloads php-fpm;
- version-compared via a sidecar marker next to the .so (idempotent).
Wired into UpdateCommand (post-update) and RootSignalsCronJob (hourly self-heal),
not LB-stripped, so every node converges. Proven on the canary LB: none -> 2.1.0
(openssl3), config_set_redis appears, StatusCommand::configureRedisLb points Redis
at main, RedisManager connects, and a viewer redirected to the LB is served
video/mp2t by the daemon (/connections + /rates populated, php-fpm flat at 1).
Nothing pulled or bootstrapped the daemon: the panel archive doesn't ship the
binary (it's a GitHub-release asset via `fanout_binary`), and `fanout_binary`
was invoked by no installer, updater or cron. So a fresh node/LB never got the
daemon, and an updated panel kept an old one. Wire it up:
- UpdateCommand: after a panel update, run `console.php fanout_binary` (root,
background, best-effort) so the daemon is refreshed to match the new version.
- RootSignalsCronJob (root, every node incl. LBs): hourly self-heal — throttled
`fanout_binary` via a stamp, first pass immediate, so a fresh install/LB gets
the daemon within a minute and version drift is corrected.
- service: the keepalive loop now runs UNCONDITIONALLY and re-checks for the
binary each 2s pass instead of gating the whole block on `-x` at boot. This
closes the bootstrap gap — fanout_binary only pkills and relies on a running
keepalive to respawn, which didn't exist on a node whose binary arrived after
boot; now the loop launches the daemon within ~2s of the binary appearing.
fanout_binary is idempotent (downloads only on a version mismatch, only when
GitHub is reachable), so polling is safe. php -l + make gates green.
Apply 'make cs-fix' — 493 files. Mechanical, import-block only:
- sort use statements alphabetically (class/function/const grouped);
- drop imports left unused by the PSR-4 migration (e.g. classes referenced by
leading-backslash FQCN whose redundant 'use' the automated insertion had added);
- one blank line after namespace and after the import block; collapse stray
blank lines around use.
No logic changes. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; and a
temporary PHPStan pass over src/Public/Controllers confirms no still-referenced
import was removed (0 unresolved classes). 'use' after inline HTML in view
templates is valid and aliases correctly (verified) — those imports are sorted too.
Namespace all 55 Cli classes into XcVm\Cli (CommandInterface, CommandRegistry,
CronTrait, DaemonTrait), XcVm\Cli\Commands (28) and XcVm\Cli\CronJobs (23);
migration_logic.php stays procedural/global.
- console.php: switch command discovery from basename==classname + manual require
to FQCN resolution — each scan dir maps to its PSR-4 namespace, classes load via
Composer, implementsInterface(CommandInterface::class). Drop the manual
CommandInterface/CommandRegistry requires. This is the atomic switch the plan
required to land with the Cli namespacing.
- Commands/CronJobs get 'use XcVm\Cli\CommandInterface;' (implements) and the
trait imports 'use XcVm\Cli\CronTrait;'/'DaemonTrait;'; rewrite the modules'
'use CommandRegistry;' → FQCN; qualify built-ins/global (\ReflectionClass,
\PDO, \Exception, \RuntimeException, \XC_Autoloader, \XC_VM).
- Fixtures: 'use CommandRegistry;' → FQCN; InterfaceContractTest registerCommands
param-type → FQCN. phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; FQCN discovery resolves
51 classes (49 implement CommandInterface); no mangled FQCNs; re-sweep clean.
Move Core/Database into XcVm\Core\Database (DatabaseHandler, Database,
MigrationRunner, QueryHelper). First of the Core hub sub-layers.
- Namespace the 4 classes; DatabaseHandler extends Database (same namespace);
built-ins/ioncube qualified (\PDO, \PDOException, \Exception, \Throwable,
\XC_VM); Database keeps its 'use XcVm\Core\Logging\FileLogger;'.
- Add 'use XcVm\Core\Database\...;' to referencing files (DatabaseHandler 51,
Database 64, QueryHelper 29, MigrationRunner 3) + 2 test files (PHPStan does not
analyse tests/, so PHPUnit is the gate there).
- Rewrite pre-existing leading-backslash global refs (\DatabaseHandler etc., e.g.
in @param docblocks of ResellerApiDispatcher/ResellerTableRenderer) to the full
FQCN \XcVm\Core\Database\... — a 'use' import does not cover a leading-\
reference. Done with a lookbehind so FQCN continuations and use-lines are intact.
- phpstan-baseline.neon regenerated (292→292; pre-existing Database/migration_logic
findings re-anchored after class names in messages gained the namespace).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295.
Move the last two Core/Config classes into XcVm\Core\Config. SettingsManager
has the largest fan-out of the whole migration (referenced by ~224 files).
- Namespace SettingsManager (self-contained singleton, no class deps) and
SettingsRepository (\FileCache:: qualified).
- Add 'use XcVm\Core\Config\SettingsManager;' to 224 referencing files and
'use ...\SettingsRepository;' to 12 — call sites (SettingsManager::get(), etc.)
unchanged. Done with a token-based inserter (after namespace/declare/<?php,
idempotent, same-namespace files skipped).
- ToolsCommand::processRecaptcha: drop dead class_exists('SettingsManager') +
method_exists guard (always autoloadable now) → call SettingsManager::clearCache()
directly. This was the only string-literal class reference.
Core/Config is now fully namespaced (ConfigReader, DomainResolver, SettingsManager,
SettingsRepository); the procedural constant files (AppConfig/Binaries/Paths)
remain global by design.
Verified: php -l clean (226 files); PHPStan no errors (baseline unchanged — it is
line-independent so the added use-lines don't disturb it); PHPUnit 295/295; all
Config FQCNs resolve via Composer; sample Public referrers lint-clean.