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:
188
web/gitpull.php
188
web/gitpull.php
@@ -18,12 +18,91 @@ if (!isset($_SESSION[$OJ_NAME.'_'.'administrator'])) {
|
|||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
$src_path = '/home/judge/src';
|
$src_path = '/home/judge/src';
|
||||||
$output = [];
|
$current_user = isset($_SESSION[$OJ_NAME.'_'.'user_id']) ? $_SESSION[$OJ_NAME.'_'.'user_id'] : 'admin';
|
||||||
$return_code = -1;
|
$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
|
function is_process_running($pid) {
|
||||||
$cmd = "sudo git -C " . escapeshellarg($src_path) . " pull 2>&1";
|
$pid = intval($pid);
|
||||||
exec($cmd, $output, $return_code);
|
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;
|
$show_title = "Git Pull - " . $OJ_NAME;
|
||||||
require("template/".$OJ_TEMPLATE."/header.php");
|
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>
|
<div class="content">服务器更新 <small style="font-size:0.6em; color:#888;">(git pull <?php echo htmlspecialchars($src_path); ?>)</small></div>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="ui <?php echo $return_code === 0 ? 'success' : 'error'; ?> message">
|
<div id="statusBox" class="ui info message">
|
||||||
<div class="header"><?php echo $return_code === 0 ? '更新成功' : '更新失败(返回码:' . $return_code . ')'; ?></div>
|
<div id="statusHeader" class="header">准备开始更新...</div>
|
||||||
<pre style="margin-top:8px; white-space:pre-wrap; word-break:break-all;"><?php echo htmlspecialchars(implode("\n", $output)); ?></pre>
|
<pre id="logBox" style="margin-top:8px; white-space:pre-wrap; word-break:break-all; max-height:420px; overflow:auto;"></pre>
|
||||||
<?php if ($return_code !== 0): ?>
|
|
||||||
<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>
|
||||||
|
|
||||||
<div style="margin-top:16px;">
|
<div style="margin-top:16px;">
|
||||||
<?php if ($return_code !== 0): ?>
|
<button id="retryBtn" class="ui primary button" style="display:none;">
|
||||||
<a class="ui primary button" href="gitpull.php">
|
|
||||||
<i class="refresh icon"></i> 重试 git pull
|
<i class="refresh icon"></i> 重试 git pull
|
||||||
</a>
|
</button>
|
||||||
<?php endif; ?>
|
|
||||||
<a class="ui button" href="javascript:history.back()">返回</a>
|
<a class="ui button" href="javascript:history.back()">返回</a>
|
||||||
</div>
|
</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>
|
||||||
</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"); ?>
|
<?php require("template/".$OJ_TEMPLATE."/footer.php"); ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user