Skip to content

Commit 9b7aef5

Browse files
Tux82claude
andcommitted
Fix PHP packages: escape FPM cgroup for extension install/remove (v1.21.9)
Same root cause as the multi-php fix — dpkg triggers restart php-fpm during apt-get install/remove of extensions, killing the PHP worker mid-request. Now runs apt inside systemd-run --scope with async status polling, matching the multi-php pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 18af17a commit 9b7aef5

2 files changed

Lines changed: 75 additions & 21 deletions

File tree

api/packages.php

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,21 @@ function getInstalledExtensions(string $ver): array
100100

101101
$pkg = "php{$ver}-{$ext}";
102102
$flag = ($action === 'install') ? 'install' : 'remove';
103-
$cmd = "DEBIAN_FRONTEND=noninteractive sudo /usr/bin/apt-get {$flag} -y " . escapeshellarg($pkg) . " 2>&1";
104-
exec($cmd, $lines, $rc);
105-
$output = implode("\n", $lines);
106-
107-
if ($rc !== 0) {
108-
echo json_encode(['success' => false, 'error' => "apt-get failed (exit {$rc})", 'output' => $output]); break;
109-
}
110-
111-
echo json_encode(['success' => true, 'output' => $output]);
112-
// Flush response before restarting FPM — restart kills the panel worker
113-
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
114-
Shell::systemctl('restart', "php{$ver}-fpm");
103+
$statusFile = "/var/www/inetpanel/storage/pkg_{$flag}_{$ver}_{$ext}";
104+
// Run in a systemd scope so dpkg triggers restarting php-fpm won't kill this
105+
$aptCmd = "DEBIAN_FRONTEND=noninteractive apt-get {$flag} -y " . escapeshellarg($pkg);
106+
$wrapper = "echo running > " . escapeshellarg($statusFile)
107+
. " && chown www-data:www-data " . escapeshellarg($statusFile)
108+
. " && chmod 0666 " . escapeshellarg($statusFile)
109+
. " && RESULT=\$({$aptCmd} 2>&1); RC=\$?;"
110+
. " dpkg --configure -a < /dev/null 2>/dev/null || true;"
111+
. " if [ \$RC -ne 0 ]; then echo \"error\" > " . escapeshellarg($statusFile)
112+
. "; echo \"\$RESULT\" >> " . escapeshellarg($statusFile) . "; exit 1; fi;"
113+
. " rm -f " . escapeshellarg($statusFile) . ";"
114+
. " systemctl restart php{$ver}-fpm 2>/dev/null";
115+
$cmd = "sudo systemd-run --scope --quiet bash -c " . escapeshellarg($wrapper) . " >> /var/www/inetpanel/storage/pkg.log 2>&1 &";
116+
exec($cmd);
117+
echo json_encode(['success' => true, 'output' => 'started', 'status_file' => basename($statusFile)]);
115118
break;
116119

117120
case 'installed_versions':
@@ -126,6 +129,26 @@ function getInstalledExtensions(string $ver): array
126129
echo json_encode(['success' => true, 'data' => $result]);
127130
break;
128131

132+
case 'pkg_status':
133+
$file = basename(trim($_GET['file'] ?? ''));
134+
if (!$file || !preg_match('/^pkg_(install|remove)_[\d.]+_\w+$/', $file)) {
135+
echo json_encode(['status' => 'done']); break;
136+
}
137+
$path = "/var/www/inetpanel/storage/{$file}";
138+
if (!file_exists($path)) {
139+
echo json_encode(['status' => 'done']); break;
140+
}
141+
$st = trim(file_get_contents($path));
142+
if ($st === 'running') {
143+
echo json_encode(['status' => 'running']);
144+
} elseif (str_starts_with($st, 'error')) {
145+
echo json_encode(['status' => 'error', 'message' => substr($st, 6)]);
146+
@unlink($path);
147+
} else {
148+
echo json_encode(['status' => 'done']);
149+
}
150+
break;
151+
129152
default:
130153
echo json_encode(['success' => false, 'error' => 'Unknown action.']);
131154
}

src/php_packages.php

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,26 +142,57 @@ function renderPackages(packages, ver) {
142142
}
143143

144144
function togglePkg(ver, ext, action) {
145-
const modal = new bootstrap.Modal(document.getElementById('pkgModal'));
146-
document.getElementById('pkg-modal-title').textContent = `${action === 'install' ? 'Installing' : 'Removing'} php${ver}-${ext}…`;
147-
document.getElementById('pkg-modal-msg').textContent = 'Please wait…';
148-
modal.show();
149145
const fd = new FormData();
150146
fd.append('action', action);
151147
fd.append('version', ver);
152148
fd.append('extension', ext);
153149
fetch('/api/packages', { method: 'POST', body: fd })
154150
.then(r => r.json())
155151
.then(data => {
156-
modal.hide();
157-
if (data.success) {
152+
if (!data.success) {
153+
showAlert(data.error || `${action} failed.`, 'danger');
154+
return;
155+
}
156+
// If API returned synchronously (e.g. opcache already loaded)
157+
if (!data.status_file) {
158158
showAlert(`php${ver}-${ext} ${action}ed successfully.`);
159159
loadPackages(ver);
160-
} else {
161-
showAlert(data.error || `${action} failed.`, 'danger');
160+
return;
162161
}
162+
// Async operation — show modal and poll status file via list action
163+
const modal = new bootstrap.Modal(document.getElementById('pkgModal'));
164+
document.getElementById('pkg-modal-title').textContent = `${action === 'install' ? 'Installing' : 'Removing'} php${ver}-${ext}…`;
165+
document.getElementById('pkg-modal-msg').textContent = 'This may take a moment…';
166+
modal.show();
167+
let attempts = 0;
168+
const poll = setInterval(() => {
169+
attempts++;
170+
fetch(`/api/packages?action=pkg_status&file=${encodeURIComponent(data.status_file)}`)
171+
.then(r => r.ok ? r.json() : null)
172+
.then(d => {
173+
if (!d) return;
174+
if (d.status === 'error') {
175+
clearInterval(poll);
176+
modal.hide();
177+
showAlert(`php${ver}-${ext} ${action} failed: ${d.message || 'Unknown error'}`, 'danger');
178+
return;
179+
}
180+
if (d.status === 'done') {
181+
clearInterval(poll);
182+
modal.hide();
183+
showAlert(`php${ver}-${ext} ${action}ed successfully.`);
184+
loadPackages(ver);
185+
}
186+
})
187+
.catch(() => {});
188+
if (attempts > 40) {
189+
clearInterval(poll);
190+
modal.hide();
191+
showAlert(`php${ver}-${ext} ${action} timed out. Check logs and reload.`, 'warning');
192+
}
193+
}, 2000);
163194
})
164-
.catch(() => { modal.hide(); showAlert('Request failed.', 'danger'); });
195+
.catch(() => { showAlert('Request failed.', 'danger'); });
165196
}
166197

167198
document.getElementById('load-pkgs-btn').addEventListener('click', function () {

0 commit comments

Comments
 (0)