#!/usr/bin/php
<?php
/**
* UPBIT DAEMON MONITORING & WATCHER (Hybrid)
* 경로: /home/www/UPBIT/monitoring/daemon.php
* 대상: /home/www/DATA/UPBIT/daemon/ 하위 모든 디렉토리의 daemon_*.php
* * [기능 1: 웹 모니터링] 브라우저 접속 시 상태 확인 및 수동 제어
* [기능 2: CLI 감시자] 터미널 실행 시 무한 루프로 실시간 감시 및 자동 부활
*/
// 에러 확인용 (운영 시 0으로 변경 가능)
ini_set('display_errors', 0);
error_reporting(E_ALL);
date_default_timezone_set('Asia/Seoul');
// ==========================
// [설정] 기본 경로 및 옵션
// ==========================
$DAEMON_DIR = '/home/www/DATA/UPBIT/daemon';
$AUTO_RESURRECT_WEB = true; // 웹 접속 시에도 체크할지 여부
// ==========================
// DB 연결
// ==========================
// CLI/Web 환경 모두 호환되도록 require 경로 확인
if (file_exists('/home/www/DB/db_upbit.php')) {
require '/home/www/DB/db_upbit.php';
$pdo = $db_upbit;
} else {
// DB 파일 없으면 CLI 모드에서 에러 출력 후 종료
if (php_sapi_name() === 'cli') die("DB FILE NOT FOUND\n");
}
// ==========================
// 공통 함수 정의
// ==========================
function get_daemon_dname($d_id) {
global $pdo;
if (!$pdo) return null;
$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;
}
function get_daemon_kind($d_id) {
global $pdo;
if (!$pdo) return '';
$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_daemon_file($filename) {
if (substr($filename, -4) !== '.php') return false;
if (strpos($filename, 'daemon_') !== 0) return false;
// 이 파일(모니터링 스크립트) 자체는 제외
if ($filename === basename(__FILE__)) return false;
return true;
}
function find_proc($subPath) {
// subPath 예: market/daemon_test.php
$pattern = "php .*{$subPath}";
$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],
];
}
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];
if (count($parts) === 2) return [$parts[0], null, $parts[1]];
$first = $parts[0];
$last = $parts[count($parts) - 1];
$middle = implode('_', array_slice($parts, 1, -1));
return [$first, $middle, $last];
}
function get_status($subPath) {
$proc = find_proc($subPath);
if ($proc) {
return [
'status' => 'RUNNING',
'pid' => $proc['pid'],
'user' => $proc['user'],
'color' => '#2ecc71'
];
}
return [
'status' => 'STOPPED',
'pid' => '-',
'user' => '-',
'color' => '#ff4757'
];
}
// [핵심] 재귀 스캔 함수 (감시 제어 로직 강화)
function scan_all_daemons($baseDir) {
$results = [];
if (!is_dir($baseDir)) return $results;
$exclude_dirs = [];
// 1. [구조적 수정] 전체 하위 디렉토리를 먼저 훑어 OFF_ 접두사 파일이 있는 모든 디렉토리 추출
try {
$checkIter = new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS);
$finder = new RecursiveIteratorIterator($checkIter, RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($finder as $fileInfo) {
$f = $fileInfo->getFilename();
if (strpos($f, 'OFF_') === 0) {
// OFF_ 파일이 위치한 실제 디렉토리 경로 (subPath) 추출
$subDir = $finder->getSubPath();
if ($subDir !== '') {
// 해당 디렉토리 제외 리스트에 추가
$exclude_dirs[] = $subDir;
} else {
// 루트에 OFF_trading 등이 있는 경우 기존 호환성 유지
$dir_name = substr($f, 4);
if ($dir_name) $exclude_dirs[] = $dir_name;
}
}
}
} catch (Exception $e) { }
$exclude_dirs = array_unique($exclude_dirs);
// 2. 실제 데몬 파일 스캔
try {
$dirIter = new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS);
$iter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($iter as $fileInfo) {
try {
if (!$fileInfo->isFile()) continue;
$subPath = $iter->getSubPath();
if ($subPath !== '') {
$is_excluded = false;
foreach ($exclude_dirs as $ex) {
// 현재 경로가 제외 디렉토리거나 그 하위인 경우 즉시 제외
if ($subPath === $ex || strpos($subPath, $ex . DIRECTORY_SEPARATOR) === 0) {
$is_excluded = true;
break;
}
}
if ($is_excluded) continue;
}
$filename = $fileInfo->getFilename();
if (!is_daemon_file($filename)) continue;
$results[] = $iter->getSubPathName();
} catch (Exception $e) { continue; }
}
} catch (Exception $e) { }
sort($results);
return $results;
}
// ==================================================================
// [MODE 1] CLI 모드 (터미널 실행 시) - 실시간 감시자 역할
// ==================================================================
if (php_sapi_name() === 'cli') {
echo "========================================\n";
echo " UPBIT DAEMON WATCHER STARTED (CLI) \n";
echo "========================================\n";
echo "Monitoring Dir: {$DAEMON_DIR}\n";
echo "Target: daemon_*.php (Recursive)\n";
echo "Press Ctrl+C to stop.\n\n";
while (true) {
// 루프 때마다 다시 스캔하여 OFF_ 파일 상태를 실시간 반영
$daemons = scan_all_daemons($DAEMON_DIR);
$resurrect_count = 0;
foreach ($daemons as $subPath) {
$proc = find_proc($subPath);
if (!$proc) {
// 죽어있음 -> 부활
$fullPath = $DAEMON_DIR . '/' . $subPath;
if (is_file($fullPath)) {
$cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
exec($cmdRun);
echo "[RESURRECT] " . date('Y-m-d H:i:s') . " : {$subPath}\n";
$resurrect_count++;
}
}
}
// CPU 과부하 방지를 위한 휴식 (3초)
sleep(3);
}
exit; // CLI 모드 종료
}
// ==================================================================
// [MODE 2] WEB 모드 (브라우저 접속 시) - 모니터링 UI
// ==================================================================
$msg = '';
// 1. POST 요청 처리 (수동 제어)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// START
if (isset($_POST['start'])) {
$subPath = $_POST['start'];
$fullPath = $DAEMON_DIR . '/' . $subPath;
$filename = basename($subPath);
if (is_daemon_file($filename) && is_file($fullPath)) {
$proc = find_proc($subPath);
if (!$proc) {
$cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
exec($cmdRun, $o, $rc);
$msg = "STARTED : {$subPath}";
} else {
$msg = "ALREADY RUNNING : {$subPath}";
}
} else {
$msg = "INVALID FILE : {$subPath}";
}
}
// STOP
if (isset($_POST['stop'])) {
$subPath = $_POST['stop'];
$filename = basename($subPath);
if (is_daemon_file($filename)) {
$cmdKill = "pkill -f " . escapeshellarg($subPath);
exec($cmdKill);
usleep(200000);
$after = find_proc($subPath);
$msg = $after ? "FAILED TO STOP : {$subPath}" : "STOPPED : {$subPath}";
}
}
// UPDATE NAME
if (isset($_POST['update_name_btn'])) {
$subPath = $_POST['target_file'];
$new_name = trim($_POST['new_d_name']);
$d_id = preg_replace('/\.php$/', '', basename($subPath));
if ($pdo) {
$sql = "UPDATE daemon_record SET d_name = :nm WHERE d_id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':nm' => $new_name, ':id' => $id]);
$msg = "NAME UPDATED : " . basename($subPath);
}
}
}
// 2. 데몬 목록 스캔
$daemons = scan_all_daemons($DAEMON_DIR);
// 3. (옵션) 웹 접속 시 자동 부활
if ($AUTO_RESURRECT_WEB && !empty($daemons)) {
$web_revived = 0;
foreach ($daemons as $subPath) {
if (!find_proc($subPath)) {
$fullPath = $DAEMON_DIR . '/' . $subPath;
if (is_file($fullPath)) {
$cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
exec($cmdRun);
$web_revived++;
}
}
}
if ($web_revived > 0) {
$txt = "WEB-CHECK: {$web_revived} daemons restarted.";
$msg = $msg ? "$msg | $txt" : $txt;
usleep(100000);
}
}
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>UPBIT DAEMON MONITORING</title>
</head>
<body class="loaded">
<link rel="stylesheet" type="text/css" href="./daemon_watchman_trading_main.css">
<div class="header-area">
<h2>UPBIT DAEMON MONITORING</h2>
<div class="search-container">
<input type="text" id="daemonSearch" class="search-input" placeholder="데몬 파일명 또는 폴더명 검색...">
</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 (PATH)</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 (recursive)</td></tr>
<?php else: ?>
<?php foreach ($daemons as $subPath):
$st = get_status($subPath);
$filename = basename($subPath);
$d_id = preg_replace('/\.php$/', '', $filename);
$raw_name = get_daemon_dname($d_id);
list($name_first, $name_center, $name_last) = parse_dname_tokens($raw_name);
$form_val = get_daemon_kind($d_id);
$dirOnly = dirname($subPath);
if ($dirOnly === '.') $dirOnly = '/';
?>
<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>
<span class="file-path"><?= htmlspecialchars($dirOnly) ?>/</span>
<span class="file-name"><?= htmlspecialchars($filename) ?></span>
</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($subPath) ?>">
<button class="start">START</button>
</form>
</td>
<td>
<?php if ($st['status'] === 'RUNNING'): ?>
<form method="post">
<input type="hidden" name="stop" value="<?= htmlspecialchars($subPath) ?>">
<button class="stop">STOP</button>
</form>
<?php endif; ?>
</td>
<td>
<form method="post">
<input type="hidden" name="target_file" value="<?= htmlspecialchars($subPath) ?>">
<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>
<script>
document.getElementById('daemonSearch').addEventListener('keyup', function() {
const filter = this.value.toLowerCase();
const rows = document.querySelectorAll('#daemonTableBody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
});
</script>
</body>
</html>