DATA/WATCHMAN/watchman/daemon_watchman_trading_main.php
#!/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>