feat(web): implement async git pull with real-time status updates

- Replace synchronous git pull execution with background job system
- Add AJAX endpoints for starting and monitoring git pull operations
- Implement process tracking using PID files and exit code files
- Add real-time log display with tail reading functionality
- Improve user experience with non-blocking UI and status updates
- Maintain backward compatibility with existing sudo permissions setup
This commit is contained in:
2026-03-13 15:50:14 +08:00
parent eb89eeb78e
commit 50692cd13a

View File

@@ -18,12 +18,91 @@ if (!isset($_SESSION[$OJ_NAME.'_'.'administrator'])) {
// ====================================================
$src_path = '/home/judge/src';
$output = [];
$return_code = -1;
$current_user = isset($_SESSION[$OJ_NAME.'_'.'user_id']) ? $_SESSION[$OJ_NAME.'_'.'user_id'] : 'admin';
$job_key = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $current_user);
$job_base = sys_get_temp_dir() . '/hustoj_gitpull_' . $job_key;
$log_file = $job_base . '.log';
$pid_file = $job_base . '.pid';
$code_file = $job_base . '.code';
// 打开页面或点击重试按钮时均以 root 身份执行 git pull
$cmd = "sudo git -C " . escapeshellarg($src_path) . " pull 2>&1";
exec($cmd, $output, $return_code);
function is_process_running($pid) {
$pid = intval($pid);
if ($pid <= 0) {
return false;
}
$out = [];
$ret = 1;
exec('ps -p ' . $pid . ' >/dev/null 2>&1', $out, $ret);
return $ret === 0;
}
function read_tail($file, $maxBytes = 200000) {
if (!file_exists($file)) {
return '';
}
$size = filesize($file);
if ($size === false || $size <= $maxBytes) {
return file_get_contents($file);
}
$fp = fopen($file, 'rb');
if (!$fp) {
return '';
}
fseek($fp, -$maxBytes, SEEK_END);
$data = fread($fp, $maxBytes);
fclose($fp);
return $data === false ? '' : $data;
}
if (isset($_GET['ajax'])) {
header('Content-Type: application/json; charset=utf-8');
$action = isset($_GET['action']) ? $_GET['action'] : '';
if ($action === 'start') {
$running = false;
if (file_exists($pid_file)) {
$pid_now = trim(file_get_contents($pid_file));
$running = is_process_running($pid_now);
}
if (!$running) {
@unlink($code_file);
@unlink($log_file);
$script = 'sudo git -C ' . escapeshellarg($src_path) . ' pull > ' . escapeshellarg($log_file) . ' 2>&1; echo $? > ' . escapeshellarg($code_file);
$cmd = 'nohup sh -c ' . escapeshellarg($script) . ' >/dev/null 2>&1 & echo $!';
$out = [];
$ret = 1;
exec($cmd, $out, $ret);
$pid = isset($out[0]) ? trim($out[0]) : '';
if ($pid !== '') {
file_put_contents($pid_file, $pid);
}
}
echo json_encode(['ok' => true]);
exit;
}
if ($action === 'status') {
$pid = file_exists($pid_file) ? trim(file_get_contents($pid_file)) : '';
$running = is_process_running($pid);
$done = file_exists($code_file);
$exit_code = null;
if ($done) {
$exit_code = intval(trim(file_get_contents($code_file)));
}
$log = read_tail($log_file);
echo json_encode([
'ok' => true,
'running' => $running,
'done' => $done,
'exit_code' => $exit_code,
'log' => $log
]);
exit;
}
echo json_encode(['ok' => false, 'msg' => 'unknown action']);
exit;
}
$show_title = "Git Pull - " . $OJ_NAME;
require("template/".$OJ_TEMPLATE."/header.php");
@@ -35,26 +114,97 @@ require("template/".$OJ_TEMPLATE."/header.php");
<div class="content">服务器更新 <small style="font-size:0.6em; color:#888;">(git pull <?php echo htmlspecialchars($src_path); ?>)</small></div>
</h2>
<div class="ui <?php echo $return_code === 0 ? 'success' : 'error'; ?> message">
<div class="header"><?php echo $return_code === 0 ? '更新成功' : '更新失败(返回码:' . $return_code . ''; ?></div>
<pre style="margin-top:8px; white-space:pre-wrap; word-break:break-all;"><?php echo htmlspecialchars(implode("\n", $output)); ?></pre>
<?php if ($return_code !== 0): ?>
<div id="statusBox" class="ui info message">
<div id="statusHeader" class="header">准备开始更新...</div>
<pre id="logBox" style="margin-top:8px; white-space:pre-wrap; word-break:break-all; max-height:420px; overflow:auto;"></pre>
</div>
<div style="margin-top:16px;">
<button id="retryBtn" class="ui primary button" style="display:none;">
<i class="refresh icon"></i> 重试 git pull
</button>
<a class="ui button" href="javascript:history.back()">返回</a>
</div>
<div style="margin-top:10px; padding:10px; background:#f8f8f8; border-radius:4px; font-size:0.9em;">
<b>若提示 sudo 权限不足,请在服务器上执行以下命令(仅需一次):</b><br>
<code>echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/git" | sudo tee /etc/sudoers.d/www-data-git</code><br>
<code>sudo chmod 440 /etc/sudoers.d/www-data-git</code>
</div>
<?php endif; ?>
</div>
<div style="margin-top:16px;">
<?php if ($return_code !== 0): ?>
<a class="ui primary button" href="gitpull.php">
<i class="refresh icon"></i> 重试 git pull
</a>
<?php endif; ?>
<a class="ui button" href="javascript:history.back()">返回</a>
</div>
</div>
<script>
(function () {
var statusBox = document.getElementById('statusBox');
var statusHeader = document.getElementById('statusHeader');
var logBox = document.getElementById('logBox');
var retryBtn = document.getElementById('retryBtn');
var timer = null;
function setStatus(type, text) {
statusBox.className = 'ui ' + type + ' message';
statusHeader.textContent = text;
}
function pollStatus() {
$.getJSON('gitpull.php', { ajax: 1, action: 'status', _: Date.now() }, function (res) {
if (!res || !res.ok) {
setStatus('error', '获取状态失败');
retryBtn.style.display = 'inline-block';
return;
}
logBox.textContent = res.log || '';
logBox.scrollTop = logBox.scrollHeight;
if (res.done) {
if (res.exit_code === 0) {
setStatus('success', '更新成功');
retryBtn.style.display = 'none';
} else {
setStatus('error', '更新失败(返回码:' + res.exit_code + '');
retryBtn.style.display = 'inline-block';
}
if (timer) {
clearInterval(timer);
timer = null;
}
} else {
setStatus('info', res.running ? '更新中,请稍候...' : '任务启动中...');
retryBtn.style.display = 'none';
}
}).fail(function () {
setStatus('error', '请求失败,请重试');
retryBtn.style.display = 'inline-block';
if (timer) {
clearInterval(timer);
timer = null;
}
});
}
function startJob() {
setStatus('info', '正在启动更新任务...');
logBox.textContent = '';
retryBtn.style.display = 'none';
$.getJSON('gitpull.php', { ajax: 1, action: 'start', _: Date.now() }, function () {
pollStatus();
if (timer) {
clearInterval(timer);
}
timer = setInterval(pollStatus, 1000);
}).fail(function () {
setStatus('error', '任务启动失败');
retryBtn.style.display = 'inline-block';
});
}
retryBtn.addEventListener('click', function () {
startJob();
});
startJob();
})();
</script>
<?php require("template/".$OJ_TEMPLATE."/footer.php"); ?>