Files
hustoj/web/gitpull.php
klarkxy 50692cd13a 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
2026-03-13 15:50:14 +08:00

211 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once('./include/db_info.inc.php');
require_once('./include/const.inc.php');
require_once('./include/memcache.php');
require_once('./include/setlang.php');
// 仅允许管理员访问
if (!isset($_SESSION[$OJ_NAME.'_'.'administrator'])) {
$view_errors = "Permission denied.";
require("template/".$OJ_TEMPLATE."/error.php");
exit(0);
}
// ====================================================
// 前置配置(在服务器上执行一次):
// echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/git" | sudo tee /etc/sudoers.d/www-data-git
// sudo chmod 440 /etc/sudoers.d/www-data-git
// ====================================================
$src_path = '/home/judge/src';
$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';
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");
?>
<div class="ui segment" style="margin-top:20px;">
<h2 class="ui header">
<i class="cloud download icon"></i>
<div class="content">服务器更新 <small style="font-size:0.6em; color:#888;">(git pull <?php echo htmlspecialchars($src_path); ?>)</small></div>
</h2>
<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>
</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"); ?>