<?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');
$SCAN_ROOT_DIRS = [
$_SERVER['DOCUMENT_ROOT'].'/DATA/UPBIT',
$_SERVER['DOCUMENT_ROOT'].'/DATA/BYBIT',
$_SERVER['DOCUMENT_ROOT'].'/DATA/STOCK',
$_SERVER['DOCUMENT_ROOT'].'/DATA/WEBBOT',
$_SERVER['DOCUMENT_ROOT'].'/DATA/WATCHMAN',
$_SERVER['DOCUMENT_ROOT'].'/DATA/OTHER',
];
$msg = '';
// ==========================
// DB 연결
// ==========================
require $_SERVER['DOCUMENT_ROOT'].'/DB/db_upbit.php';
$pdo = $db_upbit;
// ==========================
// d_name 가져오기 함수
// ==========================
function get_daemon_dname($file) {
global $pdo;
// 경로가 포함된 파일명에서 순수 파일명만 추출하여 ID 생성
// ex: trading/daemon_abc.php -> daemon_abc
$filename = basename($file);
$d_id = preg_replace('/\.php$/', '', $filename);
$sql = "SELECT d_name FROM daemon_record WHERE d_id = :id LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $d_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['d_name'] ?? null;
}
// ==========================
// d_kind 가져오기 함수
// ==========================
function get_daemon_kind($file) {
global $pdo;
$filename = basename($file);
$d_id = preg_replace('/\.php$/', '', $filename);
$sql = "SELECT d_kind FROM daemon_record WHERE d_id = :id LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $d_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['d_kind'] ?? '';
}
// 파일명 유효성 검사 (경로 제외, 파일명만 체크)
function is_valid_daemon_file($file) {
return (bool)preg_match('/^daemon_[a-zA-Z0-9_\-]+\.php$/', basename($file));
}
function whoami_web() {
$out = [];
@exec('whoami', $out);
return $out[0] ?? 'unknown';
}
function find_proc($file) {
// user,pid,cmd 한 줄만 잡기
// $file에 경로가 포함되어 있으면 해당 경로까지 매칭 (더 정확함)
$pattern = "php .*{$file}";
$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'])) {
$file = $_POST['start']; // 전체 경로
$path = $file;
if (is_valid_daemon_file($file) && is_file($path)) {
$proc = find_proc($file);
if (!$proc) {
$cmdRun = "nohup php " . escapeshellarg($path) . " > /dev/null 2>&1 &";
exec($cmdRun, $o, $rc);
$msg = "STARTED : {$file} (rc={$rc})";
} else {
$msg = "ALREADY RUNNING : {$file} (user={$proc['user']} pid={$proc['pid']})";
}
} else {
$msg = "INVALID FILE : {$file}";
}
}
// [STOP]
if (isset($_POST['stop'])) {
$file = $_POST['stop']; // 전체 경로
if (is_valid_daemon_file($file)) {
$proc = find_proc($file);
$cmdKill = "pkill -f " . escapeshellarg($file);
exec($cmdKill, $o, $rc);
$after = find_proc($file);
$webUser = whoami_web();
$beforeInfo = $proc ? "before(user={$proc['user']} pid={$proc['pid']})" : "before(none)";
$afterInfo = $after ? "after(user={$after['user']} pid={$after['pid']})" : "after(none)";
$msg = "STOP TRY : {$file} | webUser={$webUser} | {$beforeInfo} | rc={$rc} | {$afterInfo}";
} else {
$msg = "INVALID FILE : {$file}";
}
}
// [UPDATE NAME]
if (isset($_POST['update_name_btn'])) {
$file = $_POST['target_file'];
$new_name = trim($_POST['new_d_name']);
$d_id = preg_replace('/\.php$/', '', basename($file));
$sql = "UPDATE daemon_record SET d_name = :nm WHERE d_id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':nm' => $new_name, ':id' => $d_id]);
$msg = "NAME UPDATED : " . basename($file) . " -> " . htmlspecialchars($new_name);
}
}
// ==========================
// 파일 스캔 로직 (Recursive)
// ==========================
$daemons = [];
foreach ($SCAN_ROOT_DIRS as $scan_dir) {
if (!is_dir($scan_dir)) continue;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($scan_dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $info) {
if (!$info->isFile()) continue;
if (!is_valid_daemon_file($info->getFilename())) continue;
$daemons[] = $scan_dir . '/' . $iterator->getSubPathName();
}
}
$daemons = array_values(array_unique($daemons));
sort($daemons);
// ==========================
// [추가 기능] 디렉토리 감시 상태 스캔
// ==========================
$dir_watch_status = [];
foreach ($SCAN_ROOT_DIRS as $scan_dir) {
if (!is_dir($scan_dir)) continue;
$directories_to_check = [$scan_dir];
$dir_iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($scan_dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($dir_iterator as $info) {
if ($info->isDir()) {
$directories_to_check[] = $info->getPathname();
}
}
foreach ($directories_to_check as $directory_path) {
$entries = @scandir($directory_path);
if ($entries === false) continue;
$has_off_file = false;
foreach ($entries as $entry) {
if (strpos($entry, 'OFF_') === 0) {
$has_off_file = true;
break;
}
}
$relative_path = trim(str_replace($scan_dir, '', $directory_path), '/\\');
$display_path = basename($scan_dir) . ($relative_path !== '' ? '/' . str_replace('\\', '/', $relative_path) : '');
$dir_watch_status[] = [
'path' => $display_path,
'status' => $has_off_file ? 'OFF' : 'ON'
];
}
}
usort($dir_watch_status, function($a, $b) {
return strcmp($a['path'], $b['path']);
});
function get_status($file) {
$proc = find_proc($file);
if ($proc) {
return [
'status' => 'RUNNING',
'pid' => $proc['pid'],
'user' => $proc['user'],
'color' => '#2ecc71'
];
}
return [
'status' => 'STOPPED',
'pid' => '-',
'user' => '-',
'color' => '#ff4757'
];
}
// ==========================
// d_name 파싱: 첫/가운데/마지막 토큰
// ==========================
function parse_dname_tokens($d_name) {
if ($d_name === null || $d_name === '') {
return [null, null, null];
}
$parts = explode('_', $d_name);
if (count($parts) === 1) {
return [null, $parts[0], null];
} elseif (count($parts) === 2) {
return [$parts[0], null, $parts[1]];
} else {
$first = $parts[0];
$last = $parts[count($parts) - 1];
$middle = implode('_', array_slice($parts, 1, -1));
return [$first, $middle, $last];
}
}
// 헤더 부분 포함
require_once $_SERVER['DOCUMENT_ROOT'].'/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>UPBIT DAEMON MONITORING</title>
</head>
<body>
<link rel="stylesheet" type="text/css" href="./daemon.css">
<div class="page-loader" id="pageLoader"><div class="spinner"></div></div>
<div class="header-area">
<h2><i class="fa-solid fa-video"></i> DAEMON MONITORING - WATCHMAN</h2>
<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</th>
<th>FORM</th>
<th>DAEMON FILE</th>
<th>STATUS</th>
<th>PID</th>
<th>PROC USER</th>
<th>START</th>
<th>STOP</th>
<th>NAME EDIT</th>
</tr>
</thead>
<tbody id="daemonTableBody">
<?php if (empty($daemons)): ?>
<tr><td colspan="9" style="text-align:center; padding:50px; color:var(--text-dim);">NO DAEMON FOUND</td></tr>
<?php else: ?>
<?php foreach ($daemons as $d):
$st = get_status($d);
$raw_name = get_daemon_dname($d);
list($name_first, $name_center, $name_last) = parse_dname_tokens($raw_name);
$form_val = get_daemon_kind($d);
?>
<tr>
<td>
<?php if ($name_first): ?>
<span class="badge-kind badge-kind-first"><?= htmlspecialchars($name_first) ?></span>
<?php endif; ?>
<?php if ($name_center): ?>
<span class="kind-center-text"><?= htmlspecialchars($name_center) ?></span>
<?php endif; ?>
<?php if ($name_last): ?>
<span class="badge-kind badge-kind-last"><?= htmlspecialchars($name_last) ?></span>
<?php endif; ?>
</td>
<td style="color:var(--text-dim)"><?= htmlspecialchars($form_val) ?></td>
<td style="font-family: monospace; color: #60a5fa;" class="file-name"><?= htmlspecialchars($d) ?></td>
<td>
<?php if ($st['status'] === 'RUNNING'): ?>
<span class="status-running"><?= $st['status'] ?></span>
<?php else: ?>
<span style="color:var(--danger); font-weight:700;"><?= $st['status'] ?></span>
<?php endif; ?>
</td>
<td><code><?= 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>
<form method="post">
<input type="hidden" name="target_file" value="<?= htmlspecialchars($d) ?>">
<input type="text" name="new_d_name" value="<?= htmlspecialchars($raw_name) ?>" class="input-edit" placeholder="d_name">
<button name="update_name_btn" class="btn-save">SAVE</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<!-- [추가] 디렉토리 감시 가동 상태 (가로 박스형 그리드 리스트) -->
<div class="header-area" style="margin-top: 60px;">
<h2>DIRECTORY WATCH STATUS</h2>
</div>
<div class="dir-status-list">
<?php if (empty($dir_watch_status)): ?>
<div class="dir-status-box" style="grid-column: 1/-1; justify-content: center; color: var(--text-dim);">
스캔된 디렉토리가 없습니다.
</div>
<?php else: ?>
<?php foreach ($dir_watch_status as $dir): ?>
<div class="dir-status-box">
<div class="dir-info-part">
<span class="dir-path-text">📁 <?= htmlspecialchars($dir['path']) ?></span>
<span class="dir-reason-text">
<?= $dir['status'] === 'OFF' ? '🚫 Monitoring paused by OFF_ file' : '✅ Monitoring active (standard)' ?>
</span>
</div>
<div class="dir-badge-part">
<?php if ($dir['status'] === 'OFF'): ?>
<span class="dir-status-off">WATCHER OFF</span>
<?php else: ?>
<span class="dir-status-on">WATCHER ON</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- [페이지 이동 버튼] -->
<div class="nav-action-area">
<a href="<?php echo G5_URL; ?>/_PAGE/monitoring/upbit/OFF_daemon/OFF_daemon.php" class="btn-nav">
<i class="fa-solid fa-gear"></i> 감시 ON/OFF 설정 페이지로 이동
</a>
</div>
<script>
window.addEventListener('load', function() {
const loader = document.getElementById('pageLoader');
if (loader) {
loader.classList.add('hidden');
}
document.body.classList.add('loaded');
});
// 실시간 검색 기능
document.getElementById('daemonSearch').addEventListener('keyup', function() {
const filter = this.value.toLowerCase();
const rows = document.querySelectorAll('#daemonTableBody tr');
rows.forEach(row => {
// 행 내부의 전체 텍스트 내용을 합쳐서 검색 (KIND, 파일명 등 모두 포함)
const text = row.textContent.toLowerCase();
if (text.includes(filter)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
</script>
</body>
</html>