1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:18:42 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-05 22:11:38 +02:00
parent a656f40922
commit cd2e69ad6b
12 changed files with 368 additions and 62 deletions

View File

@@ -31,6 +31,10 @@ try {
$build = BuildRunner::rerunBuild($buildId, null, $userId);
json_out(['ok' => true, 'build' => $build]);
}
if ($action === 'force_rerun') {
$build = BuildRunner::forceRerun($buildId, $userId);
json_out(['ok' => true, 'build' => $build]);
}
if ($action === 'rerun_as') {
$override = is_array($body['params'] ?? null) ? $body['params'] : [];
$build = BuildRunner::rerunBuild($buildId, $override, $userId);

View File

@@ -2,4 +2,6 @@
declare(strict_types=1);
require_once __DIR__ . '/../../src/bootstrap.php';
Auth::check();
BuildRunner::reconcileRunningBuilds();
BuildRunner::reconcileQueuedBuilds();
json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]);

View File

@@ -39,27 +39,82 @@
}
}
function updateBuildRow(row, build) {
if (!row || !build) return;
row.setAttribute('data-build-status', build.status || '');
var cell = row.querySelector('[data-build-status-cell="' + build.id + '"]');
updateBuildStatusCell(cell, build.status, build.phase);
var play = row.querySelector('.build-transport-play');
var stop = row.querySelector('.build-transport-stop');
var active = build.status === 'running' || build.status === 'queued';
if (play) {
play.setAttribute('data-build-status', build.status || '');
}
if (stop) {
stop.setAttribute('data-build-status', build.status || '');
stop.disabled = !active;
}
}
function refreshRecentBuilds() {
var tbody = document.getElementById('builds-recent-tbody');
if (!tbody) return;
fetch(base + '/api/builds.php', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok || !j.builds) return;
j.builds.forEach(function (b) {
var cell = tbody.querySelector('[data-build-status-cell="' + b.id + '"]');
updateBuildStatusCell(cell, b.status, b.phase);
});
})
.catch(function () {})
.then(function () {
setTimeout(refreshRecentBuilds, 4000);
var xhr = new XMLHttpRequest();
xhr.open('GET', base + '/api/builds.php', true);
xhr.onload = function () {
var j = null;
try { j = JSON.parse(xhr.responseText); } catch (e) { j = null; }
if (!j || !j.ok || !j.builds) return;
j.builds.forEach(function (b) {
var row = tbody.querySelector('tr[data-build-id="' + b.id + '"]');
updateBuildRow(row, b);
});
};
xhr.send();
setTimeout(refreshRecentBuilds, 1000);
}
function bindBuildListTransport() {
var tbody = document.getElementById('builds-recent-tbody');
if (!tbody) return;
tbody.addEventListener('click', function (ev) {
var play = ev.target.closest('.build-transport-play');
var stop = ev.target.closest('.build-transport-stop');
if (play) {
var pid = Number(play.getAttribute('data-build-id'));
var pst = play.getAttribute('data-build-status') || '';
if (!pid) return;
var msg = pst === 'running' || pst === 'queued'
? 'Force re-run? This stops the current run and starts a new build with the same parameters.'
: null;
if (msg && !confirm(msg)) return;
postBuildAction({ action: 'force_rerun', build_id: pid }).then(function (j) {
if (j.build && j.build.id) {
window.location.href = base + '/?view=build&id=' + j.build.id;
}
}).catch(function (err) {
alert(err.message || 'Re-run failed');
});
return;
}
if (stop) {
var sid = Number(stop.getAttribute('data-build-id'));
if (!sid || stop.disabled) return;
if (!confirm('Stop this build now?')) return;
postBuildAction({ action: 'stop', build_id: sid }).then(function () {
refreshRecentBuilds();
}).catch(function (err) {
alert(err.message || 'Stop failed');
});
}
});
}
if (document.body.getAttribute('data-view') === 'home' && document.getElementById('builds-recent-tbody')) {
setTimeout(refreshRecentBuilds, 4000);
bindBuildListTransport();
refreshRecentBuilds();
}
var themeSel = document.getElementById('theme-select');
if (themeSel) {
var cur = localStorage.getItem('crash_console_theme') || 'dark';

View File

@@ -112,6 +112,7 @@ SQL);
public static function healthSummary(): array {
self::ensureSchema();
BuildRunner::reconcileRunningBuilds();
BuildRunner::reconcileQueuedBuilds();
$pdo = Database::pdo();
$running = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'running'")->fetchColumn();
$failed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'failed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn();

View File

@@ -131,12 +131,6 @@ YAML;
}
$phase = !empty($params['ssh_debug']) ? 'ssh_debug' : 'docker';
BuildRepository::update($id, [
'status' => 'running',
'phase' => $phase,
'started_at' => gmdate('Y-m-d H:i:s'),
]);
$runner = (string) cfg('build.runner_script', '/workspace/scripts/docker-build-runner.sh');
$repo = (string) cfg('build.repo_root', '/workspace');
$gitRemote = trim((string) shell_exec(
@@ -176,7 +170,16 @@ YAML;
}
$cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
$pid = trim((string) shell_exec($cmd));
if ($pid === '' || !ctype_digit($pid)) {
self::failBuild($id, 'Build runner failed to start', $logPath);
return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed'];
}
file_put_contents($dir . '/runner.pid', $pid);
BuildRepository::update($id, [
'status' => 'running',
'phase' => $phase,
'started_at' => gmdate('Y-m-d H:i:s'),
]);
return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode];
}
@@ -195,7 +198,25 @@ YAML;
public static function stopBuild(int $id): bool {
$build = BuildRepository::getById($id);
if (!$build || ($build['status'] ?? '') !== 'running') {
if (!$build) {
return false;
}
$status = (string) ($build['status'] ?? '');
if ($status === 'queued') {
$dir = self::artifactDir($id);
$logPath = $build['log_path'] ?? ($dir . '/build.log');
if ($logPath) {
file_put_contents($logPath, "\n[builder] Stopped while queued\n", FILE_APPEND);
}
BuildRepository::update($id, [
'status' => 'cancelled',
'phase' => 'stopped',
'error_message' => 'Stopped while queued',
'finished_at' => gmdate('Y-m-d H:i:s'),
]);
return true;
}
if ($status !== 'running') {
return false;
}
$dir = self::artifactDir($id);
@@ -240,6 +261,19 @@ YAML;
return self::enqueue($params, $userId);
}
/** Stop an in-flight build (if any) and enqueue a fresh run with the same params. */
public static function forceRerun(int $id, ?int $userId = null): array {
$build = BuildRepository::getById($id);
if (!$build) {
throw new InvalidArgumentException('build_not_found');
}
$status = (string) ($build['status'] ?? '');
if (in_array($status, ['running', 'queued'], true)) {
self::stopBuild($id);
}
return self::rerunBuild($id, null, $userId);
}
/** CircleCI-style: resume from the step that failed (git → docker-build → docker-run). */
public static function detectFailedStep(int $buildId): string {
$dir = self::artifactDir($buildId);
@@ -364,6 +398,33 @@ YAML;
}
}
public static function reconcileQueuedBuilds(): void {
BuildRepository::ensureSchema();
$stmt = Database::pdo()->query("SELECT id, log_path, created_at FROM builds WHERE status = 'queued'");
if (!$stmt) {
return;
}
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id <= 0) {
continue;
}
$logPath = (string) ($row['log_path'] ?? (self::artifactDir($id) . '/build.log'));
if (self::runnerAlive($id)) {
BuildRepository::update($id, [
'status' => 'running',
'phase' => 'docker',
'started_at' => gmdate('Y-m-d H:i:s'),
]);
continue;
}
$created = strtotime((string) ($row['created_at'] ?? '')) ?: 0;
if ($created > 0 && $created < time() - 120) {
self::failBuild($id, 'Build stuck in queued (runner never started)', $logPath);
}
}
}
/** Last lines / markers from build.log for reconcile error_message (max 512 chars). */
public static function summarizeLogFailure(?string $logPath): string {
if (!$logPath || !is_file($logPath)) {

View File

@@ -4,10 +4,10 @@
<section class="card card--lift" style="margin-bottom:16px">
<h2>Ecosystem health</h2>
<ul>
<li>Docker: <strong><?= h($health['docker'] ?? 'unknown') ?></strong></li>
<ul id="build-health-list">
<li>Docker: <strong id="build-health-docker"><?= h($health['docker'] ?? 'unknown') ?></strong></li>
<li>CI image version: <code><?= h($health['ci_version'] ?? '') ?></code></li>
<li>Running builds: <?= (int) ($health['running'] ?? 0) ?></li>
<li>Running builds: <span id="build-health-running"><?= (int) ($health['running'] ?? 0) ?></span></li>
<li>Passed (7d): <?= (int) ($health['passed_7d'] ?? 0) ?> · Failed (7d): <?= (int) ($health['failed_7d'] ?? 0) ?></li>
</ul>
</section>
@@ -40,9 +40,10 @@
<section class="card card--lift">
<h2>Recent builds (last 10)</h2>
<div class="reports-table-wrap">
<table class="data-table">
<table class="data-table" id="builds-recent-table">
<thead>
<tr>
<th class="build-col-controls" aria-label="Job controls"></th>
<th>Code</th>
<th>Status</th>
<th>Phase</th>
@@ -52,13 +53,22 @@
</tr>
</thead>
<tbody id="builds-recent-tbody">
<?php foreach ($builds as $b): ?>
<tr data-build-id="<?= (int) $b['id'] ?>">
<?php foreach ($builds as $b):
$bStatus = (string) ($b['status'] ?? '');
$isActive = in_array($bStatus, ['running', 'queued'], true);
?>
<tr data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>">
<td class="build-row-controls">
<div class="build-transport-actions">
<button type="button" class="build-transport-btn build-transport-play" data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>" title="Re-run job" aria-label="Re-run build <?= h($b['build_code']) ?>"></button>
<button type="button" class="build-transport-btn build-transport-stop" data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>" title="Stop job" aria-label="Stop build <?= h($b['build_code']) ?>"<?= $isActive ? '' : ' disabled' ?>></button>
</div>
</td>
<td><a href="<?= h(Auth::basePath()) ?>/?view=build&id=<?= (int) $b['id'] ?>"><?= h($b['build_code']) ?></a></td>
<td>
<span class="build-status-cell-inner" data-build-status-cell="<?= (int) $b['id'] ?>">
<?= build_status_icon((string) ($b['status'] ?? '')) ?>
<span class="build-status-label"><?= h($b['status']) ?></span>
<?= build_status_icon($bStatus) ?>
<span class="build-status-label"><?= h($bStatus) ?></span>
</span>
</td>
<td class="build-phase-cell" data-build-phase-cell="<?= (int) $b['id'] ?>"><?= h($b['phase']) ?></td>