GNU/_PAGE/monitoring/upbit/daemon_trading/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/daemon_*.php
 */

date_default_timezone_set('Asia/Seoul');

// [설정] 감시 대상 디렉토리 정의
$DAEMON_DIR = '/home/www/DATA/UPBIT/daemon/target_day';
$TARGET_DIR = '/home/www/DATA/UPBIT/daemon/target'; // 주인님이 추가하신 보석 같은 경로
$msg = '';

// ==========================
// DB 연결
// ==========================
require '/home/www/DB/db_upbit.php';
$pdo = $db_upbit;

// ==========================
// d_name 가져오기 함수
// ==========================
function get_daemon_dname($file) {
    global $pdo;

    // daemon_abc_123.php → d_id = daemon_abc_123 (확장자 제거)
    $d_id = preg_replace('/\.php$/', '', $file);

    $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; // NULL이면 그대로
}

// ==========================
// d_kind 가져오기 함수 (FORM 컬럼)
// ==========================
function get_daemon_kind($file) {
    global $pdo;
    $d_id = preg_replace('/\.php$/', '', $file);

    $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$/', $file);
}

function whoami_web() {
    $out = [];
    @exec('whoami', $out);
    return $out[0] ?? 'unknown';
}

function find_proc($file) {
    // user,pid,cmd 한 줄만 잡기
    $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') {

    if (isset($_POST['start'])) {
        $file = basename($_POST['start']);
        
        // 두 디렉토리 중 파일이 존재하는 정확한 경로 확인
        $path = (file_exists($DAEMON_DIR . '/' . $file)) ? $DAEMON_DIR . '/' . $file : $TARGET_DIR . '/' . $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}";
        }
    }

    if (isset($_POST['stop'])) {
        $file = basename($_POST['stop']);

        if (is_valid_daemon_file($file)) {
            $proc = find_proc($file);

            // pkill 시도
            $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}";
        }
    }

    // [추가] d_name 업데이트 로직
    if (isset($_POST['update_name_btn'])) {
        $file = basename($_POST['target_file']);
        $new_name = trim($_POST['new_d_name']);
        
        // ID 추출
        $d_id = preg_replace('/\.php$/', '', $file);

        // DB UPDATE
        $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 : {$file} -> " . htmlspecialchars($new_name);
    }
}

// ==========================
// 파일 스캔 로직 (지정된 모든 디렉토리)
// ==========================
$daemons = [];
$dirs_to_scan = [$DAEMON_DIR, $TARGET_DIR];
foreach ($dirs_to_scan as $dir) {
    if (is_dir($dir)) {
        foreach (scandir($dir) as $f) {
            if ($f === '.' || $f === '..') continue;
            if (!is_valid_daemon_file($f)) continue;
            $daemons[] = $f;
        }
    }
}
sort($daemons);

// ==========================
// [추가 로직] 디렉토리 감시 상태 스캔
// ==========================
$dir_watch_status = [];
$BASE_DIR_TO_SCAN = '/home/www/DATA/UPBIT/daemon'; // 상위 디렉토리 스캔
if (is_dir($BASE_DIR_TO_SCAN)) {
    $dir_iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($BASE_DIR_TO_SCAN, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($dir_iterator as $info) {
        if ($info->isDir()) {
            $sub_dir = $dir_iterator->getSubPathName();
            $full_sub_dir = $BASE_DIR_TO_SCAN . '/' . $sub_dir;
            
            $has_off_file = false;
            if (is_dir($full_sub_dir)) {
                foreach (scandir($full_sub_dir) as $f) {
                    if (strpos($f, 'OFF_') === 0) {
                        $has_off_file = true;
                        break;
                    }
                }
            }
            $dir_watch_status[] = ['path' => $sub_dir, '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 '/home/www/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-explosion"></i> DAEMON MONITORING - TRADING</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' ? '🚫 Found OFF_ file' : '✅ Active monitoring' ?>
            </span>
        </div>
        <div style="margin-top: 10px; display: flex; justify-content: flex-end;">
            <?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-toggle-on"></i> 감시 온/오프 설정 페이지로 이동
    </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>

<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>