diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php index 2283f53..c6ee6a1 100644 --- a/Plugin/unraid/include/partnership.php +++ b/Plugin/unraid/include/partnership.php @@ -189,11 +189,24 @@ function vv_pt_ts_peers(): array { // ── SSH helper — run a single command on a remote host ──────────────────────── +// Multiplexed, because this page makes several of these per render — a state read per partner, +// plus whatever a card asks for — and each one was paying a full handshake to a node across a real +// internet hop. Measured at about two seconds per call against 0.09s once a master is up. +// +// The socket lives in tmpfs so a reboot cannot inherit a stale one, and the path is kept short: a +// unix socket path is capped near 108 characters and ssh appends the user and host to this. function vv_pt_ssh(string $ip, string $sshKey, string $cmd, int $timeout = 4): string { if (!$ip || !$sshKey || !file_exists($sshKey)) return ''; + + $sock = rtrim(VV_CACHE_ROOT, '/') . '/ssh'; + if (!is_dir($sock)) @mkdir($sock, 0700, true); + $full = sprintf( - 'ssh -i %s -o ConnectTimeout=%d -o StrictHostKeyChecking=no -o BatchMode=yes root@%s %s 2>/dev/null', - escapeshellarg($sshKey), $timeout, escapeshellarg($ip), escapeshellarg($cmd) + 'ssh -i %s -o ConnectTimeout=%d -o StrictHostKeyChecking=no -o BatchMode=yes' + . ' -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=%s' + . ' root@%s %s 2>/dev/null', + escapeshellarg($sshKey), $timeout, escapeshellarg($sock . '/pt-%h'), + escapeshellarg($ip), escapeshellarg($cmd) ); return shell_exec($full) ?: ''; } @@ -513,11 +526,27 @@ function vv_pt_nodes(): array { $apiKeySet = false; $apiKeyPreview = ''; if ($isMe) { - $hn = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk'; - $keyName = 'Varaverk ' . $hn; - $apiOut = shell_exec('/usr/local/sbin/unraid-api apikey --name ' . escapeshellarg($keyName) . ' --json /dev/null'); - $apiData = json_decode(trim($apiOut ?? ''), true); - if (is_array($apiData) && !empty($apiData['key'])) { + // Cached for a minute, because `unraid-api apikey` is not a file read — it starts + // Unraid's Node CLI, measured at 1.98s, and it ran on every render of this page. That + // was the single largest cost in assembling it, and this page is the one that opens + // the mesh chat, so it was two seconds in front of a conversation every time. + // + // Still reads the key store rather than the conf, so a key deleted in Unraid's own UI + // is still reflected — a minute later rather than instantly, which is the trade. The + // preview is eight characters of a key that changes when someone deliberately rotates + // it; nobody is watching it to the second. + $apiData = vv_cache_read('pt_apikey', 60); + if ($apiData === null) { + $hn = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk'; + $keyName = 'Varaverk ' . $hn; + $apiOut = shell_exec('/usr/local/sbin/unraid-api apikey --name ' . escapeshellarg($keyName) . ' --json /dev/null'); + $decoded = json_decode(trim($apiOut ?? ''), true); + // Cached either way. A host with no key would otherwise pay the two seconds on + // every render forever, which is the case that needs the cache most. + $apiData = is_array($decoded) ? $decoded : []; + vv_cache_write('pt_apikey', $apiData); + } + if (!empty($apiData['key'])) { $apiKeySet = true; $apiKeyPreview = substr($apiData['key'], 0, 8) . '...' . substr($apiData['key'], -4); }