<?php
include_once('./_common.php');
if (!defined('_GNUBOARD_')) exit;
if (!$is_admin) exit;
/**
* UPBIT DAEMON MONITORING PAGE
* 경로: /home/www/UPBIT/monitoring/daemon.php
* 대상: /home/www/UPBIT/daemon/ 하위 모든 디렉토리 포함
*/
date_default_timezone_set('Asia/Seoul');
// 데몬이 위치한 기본 디렉토리
$DAEMON_DIR = '/home/www/DATA/UPBIT/container';
$msg = '';
// ==========================
// DB 연결
// ==========================
require '/home/www/DB/db_upbit.php';
$pdo = $db_upbit;
// ==========================
// [함수] 테이블 코멘트 가져오기 (KIND 대체)
// ==========================
function get_table_comment($file) {
global $pdo;
$table_name = preg_replace('/\.php$/', '', basename($file));
if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) return '';
try {
$stmt = $pdo->prepare("SHOW TABLE STATUS LIKE :table");
$stmt->execute([':table' => $table_name]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['Comment'] ?? '';
} catch (Exception $e) {
return '';
}
}
// [함수] 코멘트 파싱 (언더바 기준 분리 및 뱃지 디자인용)
function parse_comment_tokens($comment) {
if ($comment === null || $comment === '') return [null, null, null];
$parts = explode('_', $comment);
$count = count($parts);
if ($count === 1) return [$parts[0], null, null];
if ($count === 2) return [$parts[0], null, $parts[1]];
$first = $parts[0];
$last = $parts[$count - 1];
$middle = implode('_', array_slice($parts, 1, -1));
return [$first, $middle, $last];
}
function is_valid_daemon_file($file) {
return (bool)preg_match('/^daemon_[a-zA-Z0-9_\-]+\.php$/', basename($file));
}
/**
* 프로세스 생존 확인 함수 (Full Path 기준)
*/
function find_proc($subPath) {
global $DAEMON_DIR;
$fullPath = $DAEMON_DIR . '/' . $subPath;
// 절대 경로를 포함하여 검색해야 오작동이 없습니다.
$pattern = "php .*{$fullPath}";
$cmd = "ps -eo user,pid,cmd | grep " . escapeshellarg($pattern) . " | grep -v grep";
$out = [];
exec($cmd, $out);
if (empty($out)) return null;
$cols = preg_split('/\s+/', trim($out[0]), 3);
return [
'user' => $cols[0] ?? '-',
'pid' => $cols[1] ?? '-',
'cmd' => $cols[2] ?? '',
'raw' => $out[0],
];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// START (부활)
if (isset($_POST['start'])) {
$subPath = $_POST['start'];
$fullPath = $DAEMON_DIR . '/' . $subPath;
if (is_valid_daemon_file($subPath) && is_file($fullPath)) {
if (!find_proc($subPath)) {
// 절대 경로로 실행
$cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
exec($cmdRun);
$msg = "STARTED : " . basename($subPath);
} else {
$msg = "ALREADY RUNNING : " . basename($subPath);
}
}
}
// STOP (중지 로직 오류 수정 및 강화)
if (isset($_POST['stop'])) {
$subPath = $_POST['stop'];
$fullPath = $DAEMON_DIR . '/' . $subPath;
if (is_valid_daemon_file($subPath)) {
// pkill -f 는 정규표현식 패턴으로 프로세스를 찾습니다.
// 실행 시 사용한 'php <절대경로>' 형식을 정확히 조준합니다.
$pattern = "php .*{$fullPath}";
$cmdKill = "pkill -f " . escapeshellarg($pattern);
exec($cmdKill);
// 종료 확인을 위해 잠시 대기 (0.3초)
usleep(300000);
if (!find_proc($subPath)) {
$msg = "STOPPED : " . basename($subPath);
} else {
// 일반 종료 실패 시 강제 종료(SIGKILL) 시도
$cmdKillForce = "pkill -9 -f " . escapeshellarg($pattern);
exec($cmdKillForce);
usleep(200000);
if (!find_proc($subPath)) {
$msg = "FORCE STOPPED : " . basename($subPath);
} else {
$msg = "FAILED TO STOP : " . basename($subPath);
}
}
}
}
// UPDATE COMMENT
if (isset($_POST['update_comment_btn'])) {
$file = $_POST['target_file'];
$new_comment = trim($_POST['new_comment']);
$table_name = preg_replace('/\.php$/', '', basename($file));
if (preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) {
try {
$sql = "ALTER TABLE `{$table_name}` COMMENT = " . $pdo->quote($new_comment);
$pdo->exec($sql);
$msg = "COMMENT UPDATED : {$table_name}";
} catch (Exception $e) {}
}
}
}
// 파일 스캔
$daemons = [];
if (is_dir($DAEMON_DIR)) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($DAEMON_DIR));
foreach ($iterator as $info) {
if ($info->isFile()) {
$filename = $info->getFilename();
if (is_valid_daemon_file($filename)) {
$daemons[] = $iterator->getSubPathName();
}
}
}
}
sort($daemons);
function get_status($subPath) {
$proc = find_proc($subPath);
if ($proc) return ['status' => 'RUNNING', 'pid' => $proc['pid'], 'user' => $proc['user']];
return ['status' => 'STOPPED', 'pid' => '-', 'user' => '-'];
}
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>UPBIT DAEMON MONITORING</title>
<style>
:root {
--bg-main: #0f172a;
--bg-card: #1e293b;
--border-color: #334155;
--text-main: #f1f5f9;
--text-dim: #94a3b8;
--primary: #3b82f6;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Pretendard', sans-serif;
background: var(--bg-main); color: var(--text-main);
padding: 0; min-height: 100vh;
line-height: 1.5;
}
/* 상단 영역 보정 */
.header-area {
display: flex; justify-content: space-between; align-items: flex-end;
margin: 40px 40px 25px 40px;
}
h2 { font-size: 30px; font-weight: 700; color: #fff; letter-spacing: -0.5px; margin: 0; padding-left: 20px; }
h2 i { color: var(--danger); margin-right: 10px; }
/* 메뉴 간격 및 행간 보정 */
.nav-links { display: flex; gap: 10px; margin-top: 15px; }
.btn-nav {
display: inline-block;
background: rgba(255, 255, 255, 0.04);
color: var(--text-dim);
border: 1px solid var(--border-color);
text-decoration: none;
padding: 7px 14px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
transition: all 0.2s ease-in-out;
}
.btn-nav:hover {
background: rgba(59, 130, 246, 0.12);
border-color: var(--primary);
color: #fff;
transform: translateY(-1px);
}
.search-container { position: relative; }
.search-input {
background: var(--bg-card); border: 1px solid var(--border-color); color: #fff;
padding: 10px 15px 10px 42px; border-radius: 5px; font-size: 14px; width: 280px;
outline: none; transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-input:focus { border-color: var(--primary); width: 340px; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); }
.search-container::before {
content: '🔍'; position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
font-size: 15px; opacity: 0.6;
}
.notice {
background: rgba(59, 130, 246, 0.08); border-left: 4px solid var(--primary);
padding: 14px 22px; margin: 0 40px 25px 40px; border-radius: 4px; color: var(--primary); font-size: 14px; font-weight: 500;
}
/* 테이블 영역 */
table {
width: calc(100% - 80px); border-collapse: separate; border-spacing: 0;
background: var(--bg-card); border-radius: 6px; overflow: hidden;
border: 1px solid var(--border-color); margin: 0 40px 40px 40px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
}
th {
background: #111827; padding: 16px 20px; text-align: left;
color: var(--text-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 1.2px;
border-bottom: 1px solid var(--border-color);
}
td { padding: 14px 20px; border-bottom: 1px solid var(--border-color); font-size: 13.5px; vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255, 255, 255, 0.015); }
button { padding: 7px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }
button.start { background: var(--success); color: #fff; }
button.start:hover { background: #0d9488; }
button.stop { background: var(--danger); color: #fff; }
button.stop:hover { background: #dc2626; }
button.btn-edit { background: var(--primary); color: #fff; }
.badge-kind { padding: 3px 8px; border-radius: 4px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; }
.badge-kind-first { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
.badge-kind-last { background: rgba(168, 85, 247, 0.15); color: #a78bfa; border: 1px solid rgba(168, 85, 247, 0.3); }
.kind-center-text { font-weight: 600; color: #94a3b8; font-size: 11.5px; margin: 0 6px; }
.status-running { color: var(--success); font-weight: 800; display: flex; align-items: center; letter-spacing: 0.5px; }
.status-running::before {
content: ''; display: inline-block; width: 7px; height: 7px; background: var(--success);
border-radius: 50%; margin-right: 10px; box-shadow: 0 0 10px var(--success);
}
/* 하단 섹션 */
.footer-section { margin: 0 40px 40px 40px; padding: 30px; background: var(--bg-card); border-radius: 6px; border: 1px solid var(--border-color); }
.footer-warning { background: rgba(245, 158, 11, 0.06); border-left: 4px solid var(--warning); padding: 18px 24px; border-radius: 4px; margin-bottom: 30px; }
.footer-warning strong { color: var(--warning); display: block; margin-bottom: 6px; font-size: 15px; }
.footer-warning p { font-size: 13.5px; color: var(--text-dim); line-height: 1.6; }
/* 9개 항목 한 행 출력 설정 */
.collection-stats {
display: grid;
grid-template-columns: repeat(9, 1fr); /* 9개 고정 */
gap: 10px; /* 간격 축소 */
}
.stat-card {
background: rgba(0, 0, 0, 0.25);
padding: 15px 5px; /* 좌우 패딩 축소 */
border-radius: 5px;
border: 1px solid var(--border-color);
transition: 0.2s;
text-align: center; /* 가운데 정렬 */
}
.stat-card:hover { border-color: var(--primary); transform: translateY(-2px); }
.stat-card h4 { font-size: 10px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; white-space: nowrap; }
.stat-card .time-val { font-size: 15px; font-weight: 800; color: var(--primary); white-space: nowrap; }
.stat-card .desc { font-size: 10px; color: #4b5563; margin-top: 6px; white-space: nowrap; }
/* 모달 */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 2000; backdrop-filter: blur(6px); }
.modal-overlay.active { display: flex; }
.modal-content { background: var(--bg-card); width: 380px; padding: 30px; border-radius: 6px; border: 1px solid var(--border-color); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); }
.modal-input { width: 100%; background: #0f172a; border: 1px solid var(--border-color); color: #fff; padding: 12px; border-radius: 5px; font-size: 14px; margin: 18px 0; outline: none; transition: 0.2s; }
.modal-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
</style>
</head>
<body>
<div class="header-area">
<div class="header-left">
<h2><i class="fa-solid fa-satellite"></i> CONTAINER MONITORING - CONTAINER : HISTORY</h2>
<div class="nav-links">
<a href="/GNU/_PAGE/monitoring/upbit/daemon_market/daemon.php" class="btn-nav">마켓&통계 데몬</a>
<a href="/GNU/_PAGE/monitoring/upbit/daemon_trading/daemon.php" class="btn-nav">매매 데몬</a>
<a href="/GNU/_PAGE/monitoring/upbit/daemon_watchman/daemon.php" class="btn-nav">감시/부활 데몬</a>
</div>
</div>
<div class="search-container">
<input type="text" id="daemonSearch" class="search-input" placeholder="데몬 파일명 또는 KIND 검색...">
</div>
</div>
<?php if ($msg): ?>
<div class="notice"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>
<table>
<thead>
<tr>
<th>KIND (TABLE COMMENT)</th>
<th>DAEMON FILE</th>
<th>STATUS</th>
<th>PID</th>
<th>PROC USER</th>
<th>START</th>
<th>STOP</th>
<th>EDIT</th>
</tr>
</thead>
<tbody id="daemonTableBody">
<?php foreach ($daemons as $d):
$st = get_status($d);
$comment = get_table_comment($d);
list($k_f, $k_c, $k_l) = parse_comment_tokens($comment);
?>
<tr>
<td>
<?php if($k_f): ?><span class="badge-kind badge-kind-first"><?=htmlspecialchars($k_f)?></span><?php endif; ?>
<?php if($k_c): ?><span class="kind-center-text"><?=htmlspecialchars($k_c)?></span><?php endif; ?>
<?php if($k_l): ?><span class="badge-kind badge-kind-last"><?=htmlspecialchars($k_l)?></span><?php endif; ?>
<?php if(!$k_f && !$k_c && !$k_l): ?><span style="color:#4b5563; font-style: italic;">(No Comment)</span><?php endif; ?>
</td>
<td style="font-family: 'JetBrains Mono', monospace; color:#60a5fa; font-size: 13px;"><?=htmlspecialchars($d)?></td>
<td>
<?php if($st['status'] === 'RUNNING'): ?>
<span class="status-running">RUNNING</span>
<?php else: ?>
<span style="color:var(--danger); font-weight:800; opacity: 0.8;">STOPPED</span>
<?php endif; ?>
</td>
<td><code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px;"><?=htmlspecialchars($st['pid'])?></code></td>
<td><?=htmlspecialchars($st['user'])?></td>
<td>
<form method="post"><input type="hidden" name="start" value="<?=htmlspecialchars($d)?>"><button class="start">START</button></form>
</td>
<td>
<?php if($st['status'] === 'RUNNING'): ?>
<form method="post"><input type="hidden" name="stop" value="<?=htmlspecialchars($d)?>"><button class="stop">STOP</button></form>
<?php endif; ?>
</td>
<td>
<button class="btn-edit" onclick="openModal('<?=htmlspecialchars($d)?>', '<?=htmlspecialchars(addslashes($comment))?>')">EDIT</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="footer-section">
<div class="footer-warning">
<strong>⚠️ 데몬 수동 관리 안내</strong>
<p>현재 자동 부활 데몬이 존재하지 않습니다. 프로세스 사망 시 본 대시보드를 통한 <strong>수동 부활(START)</strong>이 필요하므로 주기적으로 상태를 확인하십시오.</p>
</div>
<h3 style="margin-bottom:18px; font-size:15px; color:#fff; font-weight: 700;">📊 1년치 데이터 수집 예상 시간 (5분 주기/200개 호출)</h3>
<div class="collection-stats">
<div class="stat-card"><h4>월봉 (Month)</h4><div class="time-val">약 5분</div><div class="desc">12개 데이터 (1회)</div></div>
<div class="stat-card"><h4>주봉 (Week)</h4><div class="time-val">약 5분</div><div class="desc">52개 데이터 (1회)</div></div>
<div class="stat-card"><h4>24시간 (일봉)</h4><div class="time-val">약 10분</div><div class="desc">365개 데이터 (2회)</div></div>
<div class="stat-card"><h4>4시간 봉</h4><div class="time-val">약 55분</div><div class="desc">2,190개 데이터 (11회)</div></div>
<div class="stat-card"><h4>1시간 봉</h4><div class="time-val">약 3.7시간</div><div class="desc">8,760개 데이터 (44회)</div></div>
<div class="stat-card"><h4>30분 봉</h4><div class="time-val">약 7.3시간</div><div class="desc">17,520개 데이터 (88회)</div></div>
<div class="stat-card"><h4>15분 봉</h4><div class="time-val">약 14.6시간</div><div class="desc">35,040개 데이터 (176회)</div></div>
<div class="stat-card"><h4>5분 봉</h4><div class="time-val">약 1.8일</div><div class="desc">105,120개 데이터 (526회)</div></div>
<div class="stat-card"><h4>1분 봉</h4><div class="time-val">약 9.1일</div><div class="desc">525,600개 데이터 (2,628회)</div></div>
</div>
</div>
<div class="modal-overlay" id="editModal">
<div class="modal-content">
<h3 style="color:#fff; font-size:17px; font-weight: 700;">EDIT TABLE COMMENT</h3>
<form method="post">
<input type="hidden" name="target_file" id="modalFile">
<input type="text" name="new_comment" id="modalComment" class="modal-input" placeholder="Enter new comment (e.g. Upbit_Trade_1m)">
<div class="modal-actions">
<button type="button" onclick="closeModal()" style="background:#4b5563; color:#fff;">CANCEL</button>
<button name="update_comment_btn" class="btn-submit" style="background:var(--primary); color:#fff;">SAVE</button>
</div>
</form>
</div>
</div>
<script>
document.getElementById('daemonSearch').addEventListener('keyup', function() {
const filter = this.value.toLowerCase();
const rows = document.querySelectorAll('#daemonTableBody tr');
rows.forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(filter) ? '' : 'none';
});
});
function openModal(file, comment) {
document.getElementById('modalFile').value = file;
document.getElementById('modalComment').value = comment;
document.getElementById('editModal').classList.add('active');
setTimeout(() => document.getElementById('modalComment').focus(), 100);
}
function closeModal() { document.getElementById('editModal').classList.remove('active'); }
</script>
</body>
</html>