GNU/_PAGE/monitoring/upbit/daemon_watchman/daemon.php
<?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>