- 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
211 lines
7.0 KiB
PHP
211 lines
7.0 KiB
PHP
<?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"); ?>
|