# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Manager                                                  ║
║  Multiprocess/Thread Task Lifecycle & RMI Management                         ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.1.0                                                             ║
║  Date    : 2026-02-11                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    Task 생명주기 관리 및 RMI(Remote Method Invocation) 통신 시스템           ║
║    - Process/Thread 모드 Task 실행                                           ║
║    - IPC Queue 기반 원격 메서드 호출                                         ║
║    - Signal Broker 이벤트 브로드캐스트                                       ║
║    - SmBlock 공유 메모리 관리                                                ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    TaskInfo      - Task 메타데이터 및 IPC 리소스                             ║
║    TaskManager   - Task 생명주기 관리자                                      ║
║    TaskRuntime   - Task 실행 래퍼 (RMI 핸들러 포함)                          ║
║    RmiClient     - 원격 메서드 호출 클라이언트                               ║
║    DirectClient  - Thread-Thread 직접 호출 클라이언트                        ║
║    _RmiProxy     - 원격 변수 지연 접근 프록시                                ║
║    _DirectProxy  - Thread 직접 호출용 프록시                                 ║
║    _SignalEmitter- Signal 체인 접근자                                        ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import Any, Dict, Optional
from dataclasses import dataclass, field
import threading
import multiprocessing
import time
import socket
import traceback
import pickle
import queue
import atexit
import signal
import os as _os
import sys as _sys

_IS_WIN32 = _sys.platform == 'win32'

from .task_performance import TimingRecorder, PerformanceCollector
from .task_signal import SignalBroker, SignalClient
from ..sm_infra import SmBlockHandler, SmRingBuffer, SmValue, SmSignalRegistry
from .task_decoration import (
    rmi_task, has_run_flag, get_signal_handlers, get_rmi_signal_forward, TaskValidator,
    _DEFAULT_RESTART_DELAY
)
from .task_error import (
    task_not_found_error, smblock_not_found_error, rmi_timeout_error,
    TaskNotFoundError, SmBlockNotFoundError, RmiTimeoutError, TaskRemovedError,
    InvokeDeadlockError, CircularRmiError
)

# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────
_MISSING = object()
_DEFAULT_RMI_TIMEOUT = 2.0
_RMI_POLL_TIMEOUT = 0.001  # 1ms (P0 최적화: 5ms → 1ms)
_WORKER_JOIN_TIMEOUT = 4.0
_ext_rb_counter = 0  # External SmRingBuffer 고유 ID 카운터
_MAX_WRITE_RETRY = 500     # request/response queue write 재시도 상한 (~50ms)
_SPIN_CHECK = 200          # response spin-wait timeout 체크 주기 (기존 2000 → 200)


# ──────────────────────────────────────────────────────────────────────────────
# Utility Functions
# ──────────────────────────────────────────────────────────────────────────────

def _check_port_in_use(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind(('0.0.0.0', port))
            return False
        except OSError:
            return True


def _check_singleton(port: int) -> bool:
    if _check_port_in_use(port):
        print(f"\n{'='*60}\n[ERROR] Port {port} in use\n{'='*60}\n")
        return False
    return True


# ──────────────────────────────────────────────────────────────────────────────
# TaskInfo
# ──────────────────────────────────────────────────────────────────────────────

@dataclass
class TaskInfo:
    task_class_name: str
    task_id: str = ""
    mode: str = "thread"
    job_class: Optional[type] = None
    injection: Dict[str, Any] = field(default_factory=dict)
    auto_restart: bool = True
    restart_delay: float = 3.0
    main_thread: bool = False  # thread 모드에서 메인 스레드 인스턴스 생성 (QObject 사용)
    job_instance: Any = None   # 메인 스레드에서 생성된 인스턴스
    # SmRingBuffer IPC (Lock-free SPSC, User-space memcpy)
    request_q: Any = None
    response_q: Any = None
    request_lock: Any = None  # multiprocessing.Lock (MPSC request writes)
    stop_flag: Any = None
    rmi_rx: Any = None
    rmi_tx: Any = None
    rmi_fail: Any = None
    rmi_time: Any = None
    rmi_time_min: Any = None
    rmi_time_max: Any = None
    restart_cnt: Any = None
    rmi_methods: Any = None
    rmi_timing: Any = None
    response_event: Any = None  # v2.6: RMI 응답 커널 이벤트 (caller 측 대기용)
    is_sleeping: Any = None    # v2.6: SmValue('B') 디스패처 대기 상태 플래그
    _ready_event: Any = None   # 동적 생성 시 초기화 완료 신호

    def __post_init__(self):
        self.task_id = self.task_id or self.task_class_name
        if not self.job_class:
            n = self.task_class_name
            self.job_class = rmi_task.get(n)
            self.mode = rmi_task.attr(n, '_tc_mode', 'thread')
            self.auto_restart = rmi_task.attr(n, '_tc_restart', True)
            self.restart_delay = rmi_task.attr(n, '_tc_delay', _DEFAULT_RESTART_DELAY)
            self.main_thread = rmi_task.attr(n, '_tc_main_thread', False)


# ──────────────────────────────────────────────────────────────────────────────
# _RmiProxy - Lazy proxy for remote variable access
# ──────────────────────────────────────────────────────────────────────────────

class _RmiProxy:
    __slots__ = ('_client', '_name', '_value', '_resolved')

    def __init__(self, client: 'RmiClient', name: str):
        sa = object.__setattr__
        sa(self, '_client', client)
        sa(self, '_name', name)
        sa(self, '_value', None)
        sa(self, '_resolved', False)

    def _resolve(self):
        if not self._resolved:
            sa = object.__setattr__
            sa(self, '_value', self._client._call('__getvar__', self._name))
            sa(self, '_resolved', True)
        return self._value

    def __call__(self, *a, **kw): return self._client._call(self._name, *a, **kw)

    def nowait(self, *a, **kw):
        """비동기 RMI 호출 (Fire-and-Forget)"""
        return self._client._call_nowait(self._name, *a, **kw)

    def __eq__(self, o): return self._resolve() == o
    def __ne__(self, o): return self._resolve() != o
    def __lt__(self, o): return self._resolve() < o
    def __le__(self, o): return self._resolve() <= o
    def __gt__(self, o): return self._resolve() > o
    def __ge__(self, o): return self._resolve() >= o

    def __add__(self, o): return self._resolve() + o
    def __radd__(self, o): return o + self._resolve()
    def __sub__(self, o): return self._resolve() - o
    def __rsub__(self, o): return o - self._resolve()
    def __mul__(self, o): return self._resolve() * o
    def __rmul__(self, o): return o * self._resolve()
    def __truediv__(self, o): return self._resolve() / o
    def __floordiv__(self, o): return self._resolve() // o
    def __mod__(self, o): return self._resolve() % o

    def __neg__(self): return -self._resolve()
    def __pos__(self): return +self._resolve()
    def __abs__(self): return abs(self._resolve())
    def __int__(self): return int(self._resolve())
    def __float__(self): return float(self._resolve())
    def __str__(self): return str(self._resolve())
    def __bool__(self): return bool(self._resolve())
    def __repr__(self): return f"_RmiProxy({self._name})"

    def __len__(self): return len(self._resolve())
    def __getitem__(self, k): return self._resolve()[k]
    def __contains__(self, i): return i in self._resolve()
    def __iter__(self): return iter(self._resolve())


# ──────────────────────────────────────────────────────────────────────────────
# DirectClient - Thread-Thread Direct Method Invocation (No IPC)
# ──────────────────────────────────────────────────────────────────────────────

class _DirectProxy:
    """Thread-Thread 직접 호출용 프록시 (RMI 없이 메서드 직접 호출)

    변수/property 접근 시 _resolve()로 실제 값을 반환.
    메서드 호출 시 __call__()로 직접 호출.
    """
    __slots__ = ('_job', '_name', '_client')

    def __init__(self, job, name: str, client: 'DirectClient' = None):
        object.__setattr__(self, '_job', job)
        object.__setattr__(self, '_name', name)
        object.__setattr__(self, '_client', client)

    def _resolve(self):
        """변수/property 값 반환 (callable이면 callable 자체를 반환하지 않음)"""
        val = getattr(self._job, self._name, None)
        return val() if callable(val) else val

    def __call__(self, *a, **kw):
        client = self._client
        # v2.1 IPC 트레이스 보고
        try:
            if client and client._broker:
                client._broker.report_trace(f"RMI::{self._name}", client._caller or "external", f"-> {client._id} (DIRECT)")
        except Exception: pass

        if client and client._debug_method:
            caller_str = client._caller or "(external)"
            args_str = f"({', '.join(repr(x)[:30] for x in a)})" if a else "()"
            print(f"[DEBUG:{caller_str}] RMI → {client._id}: {self._name}{args_str}")
        fn = getattr(self._job, self._name, None)
        if fn is None:
            raise AttributeError(f"Method '{self._name}' not found")
        return fn(*a, **kw) if callable(fn) else fn

    def nowait(self, *a, **kw):
        """Fire-and-forget (동기 호출이지만 API 호환)"""
        return self(*a, **kw)

    # 비교 연산자
    def __eq__(self, o): return self._resolve() == o
    def __ne__(self, o): return self._resolve() != o
    def __lt__(self, o): return self._resolve() < o
    def __le__(self, o): return self._resolve() <= o
    def __gt__(self, o): return self._resolve() > o
    def __ge__(self, o): return self._resolve() >= o

    # 산술 연산자
    def __add__(self, o): return self._resolve() + o
    def __radd__(self, o): return o + self._resolve()
    def __sub__(self, o): return self._resolve() - o
    def __rsub__(self, o): return o - self._resolve()
    def __mul__(self, o): return self._resolve() * o
    def __rmul__(self, o): return o * self._resolve()
    def __truediv__(self, o): return self._resolve() / o
    def __floordiv__(self, o): return self._resolve() // o
    def __mod__(self, o): return self._resolve() % o

    # 단항/변환 연산자
    def __neg__(self): return -self._resolve()
    def __pos__(self): return +self._resolve()
    def __abs__(self): return abs(self._resolve())
    def __int__(self): return int(self._resolve())
    def __float__(self): return float(self._resolve())
    def __str__(self): return str(self._resolve())
    def __bool__(self): return bool(self._resolve())
    def __repr__(self): return f"_DirectProxy({self._name})"

    # 컨테이너 연산자
    def __len__(self): return len(self._resolve())
    def __getitem__(self, k): return self._resolve()[k]
    def __contains__(self, i): return i in self._resolve()
    def __iter__(self): return iter(self._resolve())


class DirectClient:
    """Thread-Thread 직접 호출 클라이언트 (IPC 없음, 최고 성능)

    Thread 모드 task 간 호출 시 Queue를 거치지 않고
    job 인스턴스의 메서드를 직접 호출합니다.
    """
    __slots__ = ('_id', '_job', '_job_getter', '_caller', '_debug_method', '_debug_variable', '_broker')

    def __init__(self, target_id: str, job_getter, caller: str = None,
                 debug_method: bool = False, debug_variable: bool = False, broker=None):
        """
        Args:
            target_id: 대상 task ID
            job_getter: job 인스턴스를 반환하는 callable (lazy loading)
            caller: 호출자 task ID
            debug_method: method call debug tracing
            debug_variable: variable access debug tracing
            broker: v2.1 트레이스용 브로커
        """
        object.__setattr__(self, '_id', target_id)
        object.__setattr__(self, '_job', None)
        object.__setattr__(self, '_job_getter', job_getter)
        object.__setattr__(self, '_caller', caller)
        object.__setattr__(self, '_debug_method', debug_method)
        object.__setattr__(self, '_debug_variable', debug_variable)
        object.__setattr__(self, '_broker', broker)

    def _get_job(self):
        job = self._job
        if job is None:
            job = self._job_getter()
            object.__setattr__(self, '_job', job)
        return job

    def __getattr__(self, name: str):
        if name[0] == '_':
            raise AttributeError(name)
        # Variable access debug (get)
        if self._debug_variable:
            caller_str = self._caller or "(external)"
            print(f"[DEBUG:{caller_str}] VAR → {self._id}: get {name}")
        return _DirectProxy(self._get_job(), name, self)

    def __setattr__(self, name: str, value):
        if name[0] == '_':
            object.__setattr__(self, name, value)
        else:
            # Variable access debug (set)
            if self._debug_variable:
                caller_str = self._caller or "(external)"
                val_str = repr(value)[:50] + '...' if len(repr(value)) > 50 else repr(value)
                print(f"[DEBUG:{caller_str}] VAR → {self._id}: {name} = {val_str}")
            setattr(self._get_job(), name, value)

    def __repr__(self):
        return f"DirectClient({self._id})"


# ──────────────────────────────────────────────────────────────────────────────
# _SignalEmitter - Chained attribute access for signal emission
# ──────────────────────────────────────────────────────────────────────────────

class _SignalEmitter:
    """Signal 체인 접근자 (self.signal.sensor.temp.emit/on/off)"""
    __slots__ = ('_client', '_path', '_cache')

    def __init__(self, client, path: str = ""):
        object.__setattr__(self, '_client', client)
        object.__setattr__(self, '_path', path)
        object.__setattr__(self, '_cache', {})

    def __getattr__(self, name: str) -> '_SignalEmitter':
        cache = self._cache
        emitter = cache.get(name)
        if emitter is None:
            new_path = f"{self._path}.{name}" if self._path else name
            emitter = _SignalEmitter(self._client, new_path)
            cache[name] = emitter
        return emitter

    def emit(self, *args, **kwargs):
        """Signal 발신 (v2.1: 직접 호출 및 체인 호출 모두 지원)
        - self.signal.emit("name", data)           <- 직접 호출
        - self.signal.name.emit(data)              <- 체인 호출
        - self.signal.name.emit(data, qos="LOW")   <- QoS 지정
        """
        if not self._client: return
        qos = kwargs.get('qos', None)

        # 인자 개수에 따른 처리
        if len(args) == 2:
            # 직접 호출: emit("name", data)
            name, data = args[0], args[1]
            self._client.emit(name, data, qos=qos)
        elif len(args) == 1:
            if self._path:
                # 체인 호출: name.emit(data)
                self._client.emit(self._path, args[0], qos=qos)
            else:
                # 직접 호출 (인자 1개): emit("name")
                self._client.emit(args[0], None, qos=qos)
        else:
            # 인자 없음: name.emit()
            if self._path:
                self._client.emit(self._path, None, qos=qos)

    def on(self, handler):
        """Signal 구독: self.signal.job.a.on(handler)"""
        if self._client and self._path:
            self._client.on(self._path, handler)

    def off(self, handler=None):
        """Signal 구독 해제: self.signal.job.a.off(handler)"""
        if self._client and self._path:
            self._client.off(self._path, handler)


# ──────────────────────────────────────────────────────────────────────────────
# RmiClient - Remote Method Invocation Client
# ──────────────────────────────────────────────────────────────────────────────

class RmiClient:
    __slots__ = ('_id', '_req_q', '_req_lock', '_res_q', '_timeout',
                 '_tx_cnt', '_time_cnt', '_time_min', '_time_max', '_caller',
                 '_debug_method', '_debug_variable', '_broker', '_notify_event',
                 '_response_event', '_target_sleeping', '_req_seq')

    def __init__(self, target_id: str, request_q, response_q, timeout: float = _DEFAULT_RMI_TIMEOUT,
                 request_lock=None,
                 tx_counter=None, time_counter=None, time_min=None, time_max=None, caller: str = None,
                 debug_method=False, debug_variable=False, broker=None, notify_event=None,
                 response_event=None, target_sleeping=None):
        sa = object.__setattr__
        sa(self, '_id', target_id)
        sa(self, '_req_q', request_q)
        sa(self, '_req_lock', request_lock)
        sa(self, '_res_q', response_q)
        sa(self, '_timeout', timeout)
        sa(self, '_tx_cnt', tx_counter)
        sa(self, '_time_cnt', time_counter)
        sa(self, '_time_min', time_min)
        sa(self, '_time_max', time_max)
        sa(self, '_caller', caller)
        sa(self, '_debug_method', debug_method)
        sa(self, '_debug_variable', debug_variable)
        sa(self, '_broker', broker)
        sa(self, '_notify_event', notify_event)
        sa(self, '_response_event', response_event)
        sa(self, '_target_sleeping', target_sleeping)
        sa(self, '_req_seq', 0)

    def _call(self, m: str, *a, _chain: list = None, **kw):
        # Caller-side debug output
        is_var_access = m in ('__getvar__', '__setvar__')
        should_trace = self._debug_variable if is_var_access else self._debug_method
        if should_trace:
            caller_str = self._caller or "(external)"
            if is_var_access:
                var_name = a[0] if a else ''
                if m == '__setvar__':
                    val = a[1] if len(a) > 1 else None
                    val_str = repr(val)[:50] + '...' if len(repr(val)) > 50 else repr(val)
                    print(f"[DEBUG:{caller_str}] VAR → {self._id}: {var_name} = {val_str}")
                else:
                    print(f"[DEBUG:{caller_str}] VAR → {self._id}: get {var_name}")
            else:
                args_str = f"({', '.join(repr(x)[:30] for x in a)})" if a else "()"
                print(f"[DEBUG:{caller_str}] RMI → {self._id}: {m}{args_str}")

        # Fast path: skip measurement when no counters
        tx = self._tx_cnt
        if tx:
            tx.value += 1
            t0 = time.time()

        caller = self._caller
        # Chain for circular call detection (set for O(1) lookup)
        if _chain:
            chain = _chain | {caller} if caller else _chain
        else:
            chain = {caller} if caller else None

        # Minimal request dict (v2.6: _id for stale response detection)
        seq = self._req_seq + 1
        object.__setattr__(self, '_req_seq', seq)
        req = {'m': m, '_id': seq}
        if caller:
            req['c'] = caller
        if a: req['a'] = a
        if kw: req['kw'] = kw
        if chain: req['chain'] = chain

        # RMI 트레이스 보고 (v2.1 전역 공유 리스트 활용)
        try:
            if self._broker:
                self._broker.report_trace(f"RMI::{m}", self._caller or "external", f"-> {self._id} {a}")
        except Exception: pass
        

        # ★ Self-RMI 감지 (InvokeDispenser 데드락 방지)
        if caller and caller == self._id:
            raise InvokeDeadlockError(caller, self._id)

        # ── SmRingBuffer 전송 (User-space memcpy, OS 인터럽트 없음) ──
        if not caller:
            req['rq'] = self._res_q  # 외부 호출: SmRingBuffer (pickle-able)
        data = pickle.dumps(req, protocol=5)
        _lock = self._req_lock
        _write = self._req_q.write
        if _lock:
            with _lock:
                for _wr_try in range(_MAX_WRITE_RETRY):
                    if _write(data):
                        break
                    time.sleep(0)
                else:
                    raise RmiTimeoutError(self._caller or "(external)", self._id, m, self._timeout)
        else:
            for _wr_try in range(_MAX_WRITE_RETRY):
                if _write(data):
                    break
                time.sleep(0)
            else:
                raise RmiTimeoutError(self._caller or "(external)", self._id, m, self._timeout)

        # ★ InvokeDispenser 깨움 (v2.6: 조건부 — is_sleeping 체크)
        _ne = self._notify_event
        if _ne is not None:
            _ts = self._target_sleeping
            if _ts is None or _ts.value != 0:
                try: _ne.set()
                except Exception: pass

        # ── 응답 대기 (v2.6: _id 매칭으로 stale 응답 skip) ──
        _read = self._res_q.read
        _resp_evt = self._response_event
        if _resp_evt is not None:
            # v2.6: Kernel Event 대기 (CPU 0%)
            try: _resp_evt.clear()
            except Exception: pass
            _deadline = time.perf_counter() + self._timeout
            while True:
                resp = _read()
                if resp is not None:
                    r = pickle.loads(resp)
                    if r.get('_id', seq) == seq:
                        break
                    continue  # stale 응답 — skip
                remaining = _deadline - time.perf_counter()
                if remaining <= 0:
                    caller_name = self._caller or "(external)"
                    rmi_timeout_error(caller_name, self._id, m, self._timeout)
                    raise RmiTimeoutError(caller_name, self._id, m, self._timeout)
                try: _resp_evt.wait(timeout=min(remaining, 0.05))
                except Exception: time.sleep(0)
        else:
            # Fallback: 기존 Spin-wait (non-Win32 또는 response_event 없음)
            _deadline = time.perf_counter() + self._timeout
            _spin = 0
            while True:
                resp = _read()
                if resp is not None:
                    r = pickle.loads(resp)
                    if r.get('_id', seq) == seq:
                        break
                    continue  # stale 응답 — skip
                _spin += 1
                if _spin >= _SPIN_CHECK:
                    if time.perf_counter() >= _deadline:
                        caller_name = self._caller or "(external)"
                        rmi_timeout_error(caller_name, self._id, m, self._timeout)
                        raise RmiTimeoutError(caller_name, self._id, m, self._timeout)
                    time.sleep(0)
                    _spin = 0

        # Measurement (only if counters exist)
        if tx:
            elapsed = (time.time() - t0) * 1000
            tc, tmin, tmax = self._time_cnt, self._time_min, self._time_max
            if tc: tc.value += elapsed
            if tmin and elapsed < tmin.value: tmin.value = elapsed
            if tmax and elapsed > tmax.value: tmax.value = elapsed

        # Fast path for success
        e = r.get('e')
        if e:
            trace = r.get('trace')
            raise Exception(f"{e}\n--- Remote Traceback ---\n{trace}" if trace else e)
        return r.get('r')

    def _call_nowait(self, m: str, *a, _chain: list = None, **kw):
        """비동기 RMI 호출 (Fire-and-Forget)"""
        # Caller-side debug output
        is_var_access = m in ('__getvar__', '__setvar__')
        should_trace = self._debug_variable if is_var_access else self._debug_method
        if should_trace:
            caller_str = self._caller or "(external)"
            if is_var_access:
                var_name = a[0] if a else ''
                if m == '__setvar__':
                    val = a[1] if len(a) > 1 else None
                    val_str = repr(val)[:50] + '...' if len(repr(val)) > 50 else repr(val)
                    print(f"[DEBUG:{caller_str}] VAR → {self._id}: {var_name} = {val_str}")
                else:
                    print(f"[DEBUG:{caller_str}] VAR → {self._id}: get {var_name}")
            else:
                args_str = f"({', '.join(repr(x)[:30] for x in a)})" if a else "()"
                print(f"[DEBUG:{caller_str}] RMI → {self._id}: {m}{args_str}")

        tx = self._tx_cnt
        if tx: tx.value += 1
        caller = self._caller
        # Chain for circular call detection (set for O(1) lookup)
        if _chain:
            chain = _chain | {caller} if caller else _chain
        else:
            chain = {caller} if caller else None
        req = {'m': m}
        if a: req['a'] = a
        if kw: req['kw'] = kw
        if chain: req['chain'] = chain

        # ── Signal P4 큐 경유 (QoS 통합, 디스패처 v2.4) ──
        _broker = self._broker
        if _broker:
            try:
                qs = _broker._q_cache.get(self._id)
                if qs is None:
                    qs = _broker._get_queues(self._id)
                    if qs:
                        _broker._q_cache[self._id] = qs
                if qs:
                    qs[3].put_nowait({  # P4 = LOW priority
                        'name': '_rmi.nowait',
                        'source': caller or 'external',
                        'data': req,
                        'qos': {'priority': 4, 'deadline': 0.500, 'on_violation': 'IGNORE'},
                        'ts': time.time()
                    })
                    # ★ InvokeDispenser 깨움 (v2.6: 조건부)
                    _ne = self._notify_event
                    if _ne is not None:
                        _ts = self._target_sleeping
                        if _ts is None or _ts.value != 0:
                            try: _ne.set()
                            except Exception: pass
                    return
            except Exception:
                pass  # fallback to direct write

        # ── Fallback: SmRingBuffer 직접 전송 (broker 없거나 Signal 큐 실패 시) ──
        if self._req_q:
            data = pickle.dumps(req, protocol=5)
            _lock = self._req_lock
            _write = self._req_q.write
            if _lock:
                with _lock:
                    for _ in range(_MAX_WRITE_RETRY):
                        if _write(data):
                            break
                        time.sleep(0)
            else:
                for _ in range(_MAX_WRITE_RETRY):
                    if _write(data):
                        break
                    time.sleep(0)

    def __getattr__(self, m: str):
        if m[0] == '_': raise AttributeError(m)
        return _RmiProxy(self, m)

    def __setattr__(self, m: str, value):
        if m[0] == '_':
            object.__setattr__(self, m, value)
        else:
            self._call('__setvar__', m, value)

    def __repr__(self): return f"RmiClient({self._id})"


# ──────────────────────────────────────────────────────────────────────────────
# TaskManager
# ──────────────────────────────────────────────────────────────────────────────

class TaskManager:
    __slots__ = ('_tasks', '_workers', '_runtimes', '_mgr', '_monitor_port', '_monitor', '_gconfig',
                 '_exit_hook', '_measurement', '_performance', '_signal_broker', '_rmi_timeout',
                 '_task_lock', '_watchdog_running', '_watchdog_thread', '_sm_registry', '__weakref__')

    def __init__(self, gconfig):
        missing = []
        if not gconfig.data_get("app_info"): missing.append("app_info")
        if not gconfig.data_get("platform_config"): missing.append("platform_config")
        if not gconfig.data_get("task_config"): missing.append("task_config")
        if missing:
            raise ValueError(f"[TaskManager] Missing config: {', '.join(missing)}")

        self._tasks: Dict[str, TaskInfo] = {}
        self._workers: Dict[str, Any] = {}
        self._runtimes: Dict[str, 'TaskRuntime'] = {}
        self._task_lock = threading.RLock()
        self._mgr = multiprocessing.Manager()
        self._gconfig = gconfig
        self._monitor = None
        self._exit_hook = False

        platform_config = gconfig.data_get("platform_config", {})
        config = gconfig.data_get("task_config", {})
        monitor_cfg = platform_config.get("_monitor", {}) or config.get("_monitor", {})
        self._monitor_port = monitor_cfg.get("port") if monitor_cfg else None
        self._exit_hook = monitor_cfg.get("exit_hook", False) if monitor_cfg else False
        measurement_default = monitor_cfg.get("measurement", False) if monitor_cfg else False
        _pid = _os.getpid()
        try:
            self._measurement = SmValue(f"alsk{_pid}_meas", 'b', create=True, initial=int(measurement_default))
        except FileExistsError:
            self._measurement = SmValue(f"alsk{_pid}_meas", 'b', create=False)
            self._measurement._is_new = True
            self._measurement.reset(int(measurement_default))
        rmi_cfg = platform_config.get("rmi", {})
        self._rmi_timeout = rmi_cfg.get("timeout", _DEFAULT_RMI_TIMEOUT)
        mgr = self._mgr

        # LogTask 자동 등록
        log_cfg = platform_config.get("_log/log_task", {})
        if log_cfg:
            from . import task_log  # noqa: F401
            inj = {k: v for k, v in log_cfg.items() if k[0] != "@"}
            ti = TaskInfo(task_class_name="log_task", task_id="log_task", injection=inj)
            self._init_task_info(ti, mgr)
            self._tasks["log_task"] = ti

        # 사용자 Task 등록
        C, U, R = "\033[36m", "\033[4m", "\033[0m"  # Cyan, Underline, Reset
        config_path = getattr(gconfig, '_filepath', None)
        for key, cfg in config.items():
            if key.startswith("_"): continue
            # @import 처리
            if "@import" in cfg:
                try:
                    __import__(cfg["@import"])
                except ImportError as e:
                    print(f"[TaskManager] @import failed: {cfg['@import']} ({e})")
                    if config_path:
                        print(f"  {C}{U}{config_path}{R}  task: {key}")
            tid, tcn = key.split("/", 1) if "/" in key else (key, cfg.get("@task") or cfg.get("@job") or key)
            inj = {k: v for k, v in cfg.items() if k[0] != "@"}
            ti = TaskInfo(task_class_name=tcn, task_id=tid, injection=inj)
            self._init_task_info(ti, mgr)
            self._tasks[tid] = ti

        self._performance = PerformanceCollector(self._tasks, self._workers, self._measurement)
        # Signal broker: P2P Registry 전역 명부 관리자 초기화
        self._signal_broker = SignalBroker(self._mgr)
        if hasattr(self._signal_broker, 'start_relay'):
            self._signal_broker.start_relay()
        # v2.4: SmSignalRegistry (공유 메모리 비트맵 구독 관리)
        # 네임스페이스 격리: PID를 포함하여 다중 인스턴스 충돌 방지
        _reg_name = f"alaska_sig_reg_{_pid}"
        try:
            # 설계 9.3: 시작 시 기존 고아 자원 강제 정리 (Sanitization)
            self._sm_registry = SmSignalRegistry(name=_reg_name, create=True)
        except Exception:
            from multiprocessing.shared_memory import SharedMemory as _SM
            try:
                _temp_sm = _SM(name=_reg_name)
                _temp_sm.close()
                _temp_sm.unlink()
            except Exception: pass
            try:
                self._sm_registry = SmSignalRegistry(name=_reg_name, create=True)
            except Exception as e:
                print(f"[TaskManager] FATAL: SmSignalRegistry creation failed: {e}")
                self._sm_registry = None
        
        if self._sm_registry:
            self._signal_broker._sm_registry = self._sm_registry
            self._signal_broker._sm_registry_name = _reg_name
        
        # v2.3: 전역 와치독 스레드 (기본 off — config에서 watchdog: true로 활성화)
        _watchdog_enabled = platform_config.get("watchdog", False)
        if _watchdog_enabled:
            self._watchdog_running = True
            self._watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
            self._watchdog_thread.start()
        else:
            self._watchdog_running = False
            self._watchdog_thread = None

    def _watchdog_loop(self):
        """v2.3 전역 와치독: 쿨다운 및 무한 루프 방지 보완"""
        # 재시작 기록 관리 {tid: last_restart_ts}
        last_restarts = {}
        
        while self._watchdog_running:
            time.sleep(1.0)
            if not self._signal_broker: continue
            
            try:
                now = time.time()
                # 1. 하트비트 스냅샷 확보 (RuntimeError 방어)
                try:
                    hb_snap = dict(self._signal_broker._heartbeats)
                except (RuntimeError, Exception):
                    continue # 다음 주기에 시도
                
                for tid, last_alive in hb_snap.items():
                    # 2. 쿨다운 체크 (최근 10초 내 재시작했으면 건너뜀)
                    if now - last_restarts.get(tid, 0) < 10.0:
                        continue

                    # 3. 정지 판정 (3초 기준)
                    if (now - last_alive) > 3.0:
                        print(f"[Watchdog] Task '{tid}' frozen! Recovering...")
                        last_restarts[tid] = now
                        # 하트비트 즉시 갱신하여 중복 복구 차단
                        try: self._signal_broker._heartbeats[tid] = now
                        except Exception: pass
                        
                        self.restart_task(tid)
            except Exception as e:
                print(f"[Watchdog] Loop error: {e}")

    def _init_task_info(self, ti: TaskInfo, mgr):
        # SmRingBuffer IPC (User-space memcpy, OS 인터럽트 없음)
        _pid = _os.getpid()
        _sid = ti.task_id.replace('.', '_').replace('-', '_')

        def _create_rb(suffix, size=65536):
            name = f"alsk{_pid}_{_sid}_{suffix}"
            try:
                return SmRingBuffer(name, size=size, create=True)
            except FileExistsError:
                rb = SmRingBuffer(name, size=size, create=False)
                rb._is_new = True
                rb.reset()
                return rb

        def _create_sv(suffix, typecode, initial=0):
            name = f"alsk{_pid}_{_sid}_{suffix}"
            try:
                return SmValue(name, typecode, create=True, initial=initial)
            except FileExistsError:
                sv = SmValue(name, typecode, create=False)
                sv._is_new = True
                sv.reset(initial)
                return sv

        ti.request_q = _create_rb("rq")
        ti.response_q = _create_rb("rs")
        ti.request_lock = multiprocessing.Lock()
        # SmValue: User-space SharedMemory (Manager 소켓 IPC 제거)
        ti.stop_flag = _create_sv("sf", 'b', 0)
        ti.rmi_rx = _create_sv("rx", 'i', 0)
        ti.rmi_tx = _create_sv("tx", 'i', 0)
        ti.rmi_fail = _create_sv("fl", 'i', 0)
        ti.rmi_time = _create_sv("rt", 'd', 0.0)
        ti.rmi_time_min = _create_sv("mn", 'd', float('inf'))
        ti.rmi_time_max = _create_sv("mx", 'd', 0.0)
        ti.restart_cnt = _create_sv("rc", 'i', 0)
        ti.rmi_methods = mgr.dict()
        ti.rmi_timing = mgr.dict()
        # v2.6: CPU 절감 — is_sleeping 플래그 + response_event
        ti.is_sleeping = _create_sv("sl", 'B', 0)
        if _IS_WIN32:
            from ..sm_infra import SmKernelEvent
            _resp_evt_name = f"al_rmi_resp_{_pid}_{_sid}"
            try:
                ti.response_event = SmKernelEvent(_resp_evt_name, create=True, manual_reset=True)
            except OSError:
                ti.response_event = SmKernelEvent(_resp_evt_name, create=False, manual_reset=True)

    @property
    def performance(self) -> PerformanceCollector:
        return self._performance

    @property
    def gconfig(self):
        """Global configuration (read-only)"""
        return self._gconfig

    def generate_architecture_map(self) -> str:
        """v2.3 시스템 구성도 자동 생성 (Mermaid 포맷)"""
        lines = ["graph TD", "  subgraph ALASKA_SYSTEM"]
        
        # 1. 노드 정의 (TaskID [TaskClass])
        for tid, ti in self._tasks.items():
            mode_icon = "⚙️" if ti.mode == 'process' else "🧵"
            lines.append(f'    {tid}["{mode_icon} {tid} <br/>({ti.task_class_name})"]')

        # 2. RMI 의존성 (Injection 분석)
        for tid, ti in self._tasks.items():
            for key, val in ti.injection.items():
                if isinstance(val, str) and val.startswith("task:"):
                    target = val.split(":")[1]
                    lines.append(f'    {tid} -. RMI .-> {target}')

        # 3. 시그널 흐름 (on_ 메서드 분석)
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            try:
                for name in dir(cls):
                    if name.startswith("on_"):
                        sig_name = name[3:].replace("_", ".")
                        lines.append(f'    SIG(({sig_name})) --> {tid}')
            except Exception: pass
        
        lines.append("  end")
        return "\n".join(lines)

    def generate_sequence_map(self) -> str:
        """v2.3 정적 시퀀스 다이어그램 자동 생성 (Mermaid sequenceDiagram)

        코드 정적 분석: signal.emit() 호출(송신)과 on_ 메서드(수신)를 매칭하여
        실행 없이 예상 시그널 흐름을 시각화한다.
        """
        import re, inspect
        lines = ["sequenceDiagram"]

        # 1. Participant 정의 (TaskID)
        for tid in self._tasks:
            lines.append(f"  participant {tid}")

        # 2. emit → on_ 매칭 테이블 구축
        # 2a. on_ 수신 맵: signal_name → [task_id, ...]
        recv_map = {}  # {"raw.sensor.1": ["analytics_node"], ...}
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            for attr in dir(cls):
                if attr.startswith("on_"):
                    sig = attr[3:].replace("_", ".")
                    recv_map.setdefault(sig, []).append(tid)

        # 2b. emit 송신 분석: 소스코드에서 signal.emit("xxx", ...) 추출
        emit_pattern = re.compile(
            r'self\.signal\.emit\(\s*["\']([^"\']+)["\']'  # self.signal.emit("name"
            r'|'
            r'self\.signal\.([a-zA-Z_]\w*)\.emit\('         # self.signal.name.emit(
        )
        emit_map = {}  # {"sensor_node": ["raw.sensor.1", "sensor.urgent"], ...}
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            signals = []
            try:
                src = inspect.getsource(cls)
                for m in emit_pattern.finditer(src):
                    sig = m.group(1) or m.group(2).replace("_", ".")
                    if sig not in signals:
                        signals.append(sig)
            except Exception: pass
            if signals:
                emit_map[tid] = signals

        # 2c. RMI 의존성 (task: injection)
        rmi_map = {}  # {"controller_node": ["sensor_node"], ...}
        for tid, ti in self._tasks.items():
            for key, val in ti.injection.items():
                if isinstance(val, str) and val.startswith("task:"):
                    target = val.split(":")[1]
                    rmi_map.setdefault(tid, []).append((key, target))

        # 3. Mermaid 시퀀스 생성
        # 3a. RMI 점선 화살표
        for tid, deps in rmi_map.items():
            for proxy_name, target in deps:
                lines.append(f"  {tid} -->> {target}: RMI ({proxy_name})")

        # 3b. Signal 실선 화살표 (emit → on_ 매칭)
        for tid, sigs in emit_map.items():
            for sig in sigs:
                receivers = recv_map.get(sig, [])
                if receivers:
                    for rtid in receivers:
                        lines.append(f"  {tid} ->> {rtid}: {sig}")
                else:
                    lines.append(f"  Note over {tid}: {sig} (no subscriber)")

        return "\n".join(lines)

    def start_all(self, exit_on_duplicate: bool = True):
        if self._monitor_port and not _check_singleton(self._monitor_port):
            raise SystemExit(1) if exit_on_duplicate else RuntimeError(f"Port {self._monitor_port} in use")

        validator = TaskValidator()
        for tid, ti in self._tasks.items():
            validator.add(tid, ti.task_class_name)
        if not validator.validate():
            validator.print_errors()
            raise SystemExit(1)

        self._print_task_table()
        SmBlockHandler.creation(self._gconfig, self._mgr, self._tasks)

        if self._signal_broker:
            _pid = _os.getpid()
            for tid, ti in self._tasks.items():
                self._signal_broker.register_inbox(tid, mode=ti.mode)
                # v2.4: SmSignalRegistry에 태스크 등록
                if self._sm_registry:
                    self._sm_registry.register_task(tid)
                # v2.4: notify_event 생성 (SmKernelEvent/mp.Event)
                _sid = tid.replace('.', '_').replace('-', '_')
                if _IS_WIN32:
                    from ..sm_infra import SmKernelEvent
                    _evt_name = f"al_notify_{_pid}_{_sid}"
                    try:
                        ev = SmKernelEvent(_evt_name, create=True, manual_reset=True)
                    except OSError:
                        ev = SmKernelEvent(_evt_name, create=False, manual_reset=True)
                else:
                    ev = multiprocessing.Event()
                self._signal_broker._notify_events[tid] = ev
                # v2.5: 이벤트 이름 등록 (크로스프로세스 lazy re-open용)
                if _IS_WIN32:
                    self._signal_broker._notify_event_names[tid] = _evt_name
                # v2.6: is_sleeping 플래그 등록
                if ti.is_sleeping is not None:
                    self._signal_broker._sleeping_flags[tid] = ti.is_sleeping
                # 사전 구독: 모든 Task의 on_* 핸들러를 Registry에 즉시 반영
                if ti.job_class:
                    self._pre_subscribe_signals(tid, ti.job_class)

        for tid, ti in self._tasks.items():
            if tid not in self._workers:
                # main_thread=True인 경우, 메인 스레드에서 인스턴스 생성 (QObject 지원)
                if ti.mode == 'thread' and ti.main_thread and ti.job_class:
                    ti.job_instance = ti.job_class()  # 메인 스레드에서 생성
                    ti.job_instance._input_queue = queue.Queue()  # 기본 입력 큐 주입
                    if not hasattr(ti.job_instance, 'on_input'):
                        ti.job_instance.on_input = lambda data, j=ti.job_instance: j._input_queue.put_nowait(data)
                # process 모드: pickle 불가 객체만 제외 (multiprocessing.Queue는 유지)
                if ti.mode == 'process':
                    import copy
                    clean_tasks = {}
                    for t_id, t_info in self._tasks.items():
                        clean_ti = copy.copy(t_info)
                        clean_ti.job_instance = None   # unpicklable
                        clean_ti.response_event = None  # v2.6: SmKernelEvent (ctypes) — 이름으로 재연결
                        # request_q, response_q는 SmRingBuffer (SharedMemory) — pickle 가능
                        # is_sleeping은 SmValue (SharedMemory) — pickle 가능
                        clean_tasks[t_id] = clean_ti
                    clean_self_ti = clean_tasks[tid]
                    # Runtime 생성 (Manager는 전달하지 않음 - Pickle 불가)
                    w = TaskRuntime(clean_self_ti, clean_tasks, 
                                    measurement=self._measurement, 
                                    signal_broker=self._signal_broker, 
                                    rmi_timeout=self._rmi_timeout,
                                    gconfig=self._gconfig,
                                    manager=None)
                    h = multiprocessing.Process(target=w.run, daemon=True)
                else:
                    # Runtime 생성
                    w = TaskRuntime(ti, self._tasks, 
                                    measurement=self._measurement, 
                                    signal_broker=self._signal_broker, 
                                    rmi_timeout=self._rmi_timeout,
                                    gconfig=self._gconfig,
                                    manager=self)
                    h = threading.Thread(target=w.run, daemon=True)
                if ti.mode == 'thread':
                    self._runtimes[tid] = w  # thread 모드는 런타임 저장 (get_task용)
                h.start()
                self._workers[tid] = h

        if self._monitor_port and not self._monitor:
            from ..monitor.task_monitor import TaskMonitor
            self._monitor = TaskMonitor(self, self._monitor_port, gconfig=self._gconfig)
            self._monitor.start()

        if self._exit_hook:
            atexit.register(self.stop_all)
            if threading.current_thread() is threading.main_thread():
                signal.signal(signal.SIGINT, lambda s, f: self.stop_all())

    def stop_all(self, skip_monitor: bool = False):
        # Watchdog 종료
        self._watchdog_running = False
        if self._watchdog_thread:
            self._watchdog_thread.join(timeout=2.0)
            self._watchdog_thread = None
        if self._monitor and not skip_monitor:
            self._monitor.stop()
            self._monitor = None
        with self._task_lock:
            tasks_snapshot = dict(self._tasks)
            workers_snapshot = dict(self._workers)
        for ti in tasks_snapshot.values():
            ti.stop_flag.value = True
        for tid, w in workers_snapshot.items():
            w.join(timeout=_WORKER_JOIN_TIMEOUT)
            if w.is_alive() and hasattr(w, 'terminate'):
                w.terminate()
        # SharedMemory cleanup (SmRingBuffer + SmValue)
        for ti in tasks_snapshot.values():
            for res in (ti.request_q, ti.response_q, ti.response_event, ti.is_sleeping,
                        ti.stop_flag, ti.rmi_rx, ti.rmi_tx, ti.rmi_fail,
                        ti.rmi_time, ti.rmi_time_min, ti.rmi_time_max, ti.restart_cnt):
                if res and hasattr(res, 'close'):
                    try: res.close()
                    except Exception: pass
        # _measurement cleanup
        if hasattr(self._measurement, 'close'):
            try: self._measurement.close()
            except Exception: pass
        # v2.4: SmSignalRegistry cleanup
        if hasattr(self, '_sm_registry') and self._sm_registry:
            try: self._sm_registry.close()
            except Exception: pass
            self._sm_registry = None
        # v2.4: notify_events cleanup
        if self._signal_broker:
            for ev in self._signal_broker._notify_events.values():
                if hasattr(ev, 'close'):
                    try: ev.close()
                    except Exception: pass
            self._signal_broker._notify_events.clear()
        # v2.5: Signal SmRingBuffer cleanup
        if self._signal_broker:
            for tid, qs in list(self._signal_broker._q_cache.items()):
                if qs:
                    for sq in qs:
                        if hasattr(sq, 'close'):
                            try: sq.close()
                            except Exception: pass
            self._signal_broker._q_cache.clear()
        with self._task_lock:
            self._workers.clear()
            self._runtimes.clear()
        SmBlockHandler.close_all()

    def restart_task(self, task_id: str):
        """Watchdog에서 호출: frozen task 재시작"""
        with self._task_lock:
            ti = self._tasks.get(task_id)
            worker = self._workers.get(task_id)
            if not ti or not worker:
                return

            # 1. 기존 프로세스/스레드 정지
            if ti.mode == 'process':
                if hasattr(worker, 'terminate'):
                    worker.terminate()
                worker.join(timeout=1.0)
                if worker.is_alive() and hasattr(worker, 'kill'):
                    worker.kill()
            else:
                ti.stop_flag.value = True
                if isinstance(worker, threading.Thread):
                    worker.join(timeout=1.0)

            # 2. 큐 데이터 청소 (v2.5: SmRingBuffer reset 우선)
            if self._signal_broker:
                try:
                    qs = self._signal_broker._q_cache.get(task_id)
                    if qs is None:
                        qs = self._signal_broker._queues.get(task_id)
                    if qs:
                        for q in qs:
                            if hasattr(q, 'reset'):
                                q.reset()
                            else:
                                while not q.empty():
                                    try: q.get_nowait()
                                    except Exception: break
                except Exception: pass

            # 3. SmRingBuffer 초기화
            for q in (ti.request_q, ti.response_q):
                if q and hasattr(q, 'reset'):
                    try: q.reset()
                    except Exception: pass

            # 4. 상태 초기화 및 재시작
            ti.stop_flag.value = False
            ti.restart_cnt.value += 1

            if ti.mode == 'process':
                import copy
                clean_tasks = {}
                for t_id, t_info in self._tasks.items():
                    clean_ti = copy.copy(t_info)
                    clean_ti.job_instance = None
                    clean_ti.response_event = None  # v2.6: SmKernelEvent (ctypes) — 이름으로 재연결
                    clean_tasks[t_id] = clean_ti
                w = TaskRuntime(clean_tasks[task_id], clean_tasks,
                                measurement=self._measurement,
                                signal_broker=self._signal_broker,
                                rmi_timeout=self._rmi_timeout,
                                gconfig=self._gconfig,
                                manager=None)
                h = multiprocessing.Process(target=w.run, daemon=True)
            else:
                w = TaskRuntime(ti, self._tasks,
                                measurement=self._measurement,
                                signal_broker=self._signal_broker,
                                rmi_timeout=self._rmi_timeout,
                                gconfig=self._gconfig,
                                manager=self)
                h = threading.Thread(target=w.run, daemon=True)

            h.start()
            self._workers[task_id] = h
            if ti.mode == 'thread':
                self._runtimes[task_id] = w

    # ── 동적 Task 생성/삭제 (Phase 1: thread 모드 전용) ──────────────────

    def add_task(self, task_id: str, task_class_name: str,
                 injection: Dict[str, Any] = None) -> bool:
        """실행 중 Task 동적 추가 (thread 모드 전용)

        Args:
            task_id: 고유 태스크 ID
            task_class_name: @rmi_task 등록명
            injection: 의존성 주입 딕셔너리

        Returns:
            True: 성공, False: 이미 존재
        """
        with self._task_lock:
            if task_id in self._tasks:
                print(f"[TaskManager] add_task: '{task_id}' already exists")
                return False

        # TaskInfo 생성
        ti = TaskInfo(task_class_name=task_class_name, task_id=task_id,
                      injection=injection or {})
        if not ti.job_class:
            raise ValueError(f"[TaskManager] add_task: class '{task_class_name}' not registered. Use @rmi_task decorator.")
        if ti.mode == 'process':
            raise ValueError(f"[TaskManager] add_task: process mode not supported (Phase 1). Use thread mode.")

        # IPC 리소스 할당
        self._init_task_info(ti, self._mgr)

        # ready_event: 초기화 완료 대기용
        ti._ready_event = threading.Event()

        # 등록
        with self._task_lock:
            if task_id in self._tasks:  # double-check
                return False
            self._tasks[task_id] = ti

        # Signal 등록 (v2.1: Registry에 Inbox 등록 및 동기화 트리거)
        if self._signal_broker:
            self._signal_broker.register_inbox(task_id, mode=ti.mode)

        # Worker 생성 & 시작
        w = TaskRuntime(ti, self._tasks, 
                        measurement=self._measurement,
                        signal_broker=self._signal_broker, 
                        rmi_timeout=self._rmi_timeout,
                        gconfig=self._gconfig,
                        manager=self)
        h = threading.Thread(target=w.run, daemon=True)
        with self._task_lock:
            self._runtimes[task_id] = w
            self._workers[task_id] = h
        h.start()

        # 초기화 완료 대기
        if not ti._ready_event.wait(timeout=5.0):
            print(f"[W:TaskManager] add_task timeout: {task_id} (continuing async)")

        print(f"[TaskManager] Task added: {task_id} ({task_class_name})")
        return True

    def remove_task(self, task_id: str, timeout: float = 5.0) -> bool:
        """실행 중 Task 동적 삭제

        Args:
            task_id: 삭제할 태스크 ID
            timeout: 종료 대기 시간 (초)

        Returns:
            True: 성공, False: 존재하지 않음
        """
        with self._task_lock:
            ti = self._tasks.get(task_id)
            worker = self._workers.get(task_id)
        if not ti:
            print(f"[TaskManager] remove_task: '{task_id}' not found")
            return False

        # 의존성 경고
        with self._task_lock:
            for tid, t in self._tasks.items():
                if tid == task_id:
                    continue
                for k, v in t.injection.items():
                    if isinstance(v, str) and v == f"client:{task_id}":
                        print(f"[W:TaskManager] Task '{tid}' depends on '{task_id}' (injection: {k})")

        # 종료 요청
        ti.stop_flag.value = True

        # Signal 해제
        if self._signal_broker:
            self._signal_broker.unregister(task_id)

        # Worker 종료 대기
        if worker:
            worker.join(timeout=timeout)
            if worker.is_alive():
                print(f"[W:TaskManager] remove_task: '{task_id}' join timeout")

        # SharedMemory close (SmRingBuffer + SmValue)
        for res in (ti.request_q, ti.response_q,
                    ti.stop_flag, ti.rmi_rx, ti.rmi_tx, ti.rmi_fail,
                    ti.rmi_time, ti.rmi_time_min, ti.rmi_time_max, ti.restart_cnt):
            if res and hasattr(res, 'close'):
                try: res.close()
                except Exception: pass

        # 등록 해제
        with self._task_lock:
            self._tasks.pop(task_id, None)
            self._workers.pop(task_id, None)
            self._runtimes.pop(task_id, None)

        print(f"[TaskManager] Task removed: {task_id}")
        return True

    def clear_stats(self):
        inf = float('inf')
        with self._task_lock:
            snapshot = list(self._tasks.values())
        for ti in snapshot:
            ti.rmi_rx.value = ti.rmi_tx.value = ti.rmi_fail.value = 0
            ti.rmi_time.value = ti.rmi_time_max.value = 0.0
            ti.rmi_time_min.value = inf
            ti.restart_cnt.value = 0
            ti.rmi_methods.clear()
            ti.rmi_timing.clear()

    def get_status(self) -> Dict[str, Dict[str, Any]]:
        result = {}
        inf = float('inf')
        with self._task_lock:
            tasks_snapshot = dict(self._tasks)
            workers_snapshot = dict(self._workers)
        for tid, ti in tasks_snapshot.items():
            w = workers_snapshot.get(tid)
            try:
                tx = int(ti.rmi_tx.value)
                tt = float(ti.rmi_time.value)
                tmin = float(ti.rmi_time_min.value)
                result[tid] = {
                    'class': ti.task_class_name, 'mode': ti.mode,
                    'alive': w.is_alive() if w else False,
                    'rmi_rx': int(ti.rmi_rx.value), 'rmi_tx': tx, 'rmi_fail': int(ti.rmi_fail.value),
                    'rmi_avg': tt / tx if tx else 0.0,
                    'rmi_min': 0.0 if tmin == inf else tmin,
                    'rmi_max': float(ti.rmi_time_max.value),
                    'rxq_size': ti.request_q._used() if not getattr(ti.request_q, '_closed', True) else 0,
                    'txq_size': ti.response_q._used() if not getattr(ti.response_q, '_closed', True) else 0,
                    'restart': int(ti.restart_cnt.value),
                    'rmi_methods': dict(ti.rmi_methods),
                }
            except Exception:
                result[tid] = {
                    'class': ti.task_class_name, 'mode': ti.mode,
                    'alive': w.is_alive() if w else False,
                }
        return result

    def get_client(self, task_id: str) -> RmiClient:
        """Task RMI 클라이언트 생성 (External용 SmRingBuffer response)"""
        global _ext_rb_counter
        ti = self._tasks.get(task_id)
        if not ti:
            task_not_found_error(task_id, self._tasks, "get_client")
            raise TaskNotFoundError(task_id, self._tasks)
        # External response SmRingBuffer
        _ext_rb_counter += 1
        _pid = _os.getpid()
        _sid = task_id.replace('.', '_').replace('-', '_')
        _name = f"alsk{_pid}_ext{_ext_rb_counter}_{_sid}"
        try:
            response_rb = SmRingBuffer(_name, size=65536, create=True)
        except FileExistsError:
            response_rb = SmRingBuffer(_name, size=65536, create=False)
            response_rb._is_new = True
            response_rb.reset()
        # ★ Target의 notify_event 조회 (InvokeDispenser 깨움용)
        _ne = None
        if self._signal_broker:
            _ne = self._signal_broker._notify_events.get(task_id)
            if _ne is None:
                _ne = self._signal_broker._notify_cache.get(task_id)
        return RmiClient(
            target_id=task_id,
            request_q=ti.request_q,
            request_lock=ti.request_lock,
            response_q=response_rb,
            timeout=self._rmi_timeout,
            broker=self._signal_broker,
            notify_event=_ne,
            target_sleeping=ti.is_sleeping
        )

    def get_task(self, task_id: str):
        """thread 모드 Task 인스턴스 반환 (Qt Signal 직접 접근용)"""
        runtime = self._runtimes.get(task_id)
        if not runtime:
            ti = self._tasks.get(task_id)
            if not ti:
                self._print_get_task_error(task_id, "Task not found")
                return None
            if ti.mode == 'process':
                self._print_get_task_error(task_id, "process mode Task. Use get_client() instead.")
                return None
            # main_thread=True인 경우 start_all()에서 이미 인스턴스 생성됨
            if ti.job_instance is not None:
                return ti.job_instance
            return None  # 아직 시작되지 않음
        return getattr(runtime, 'job', None) or getattr(self._tasks.get(task_id), 'job_instance', None)

    def _print_get_task_error(self, task_id: str, message: str):
        """get_task() 오류 메시지 출력 (배너 + 호출 위치 링크)"""
        import inspect
        # ANSI 색상 코드
        B = "\033[92m"  # 밝은 녹색 (테두리)
        YELLOW = "\033[33m"
        CYAN = "\033[36m"
        UNDERLINE = "\033[4m"
        RESET = "\033[0m"
        W = 72  # 배너 너비
        # 호출 스택에서 get_task() 호출 위치 찾기
        # _print_get_task_error → get_task → caller
        frame = inspect.currentframe()
        try:
            caller_frame = frame.f_back.f_back  # caller of get_task()
            filename = caller_frame.f_code.co_filename
            lineno = caller_frame.f_lineno
        except (AttributeError, ValueError):
            filename, lineno = "unknown", 0
        finally:
            del frame
        # 배너 출력 (가는 실선)
        print()
        print(f"{B}┌{'─' * W}┐{RESET}")
        # 오류 메시지
        err_text = f"get_task('{task_id}'): {message}"
        err_text = err_text if len(err_text) <= W - 4 else err_text[:W - 7] + "..."
        print(f"{B}│{RESET}  {YELLOW}{err_text}{RESET}{' ' * (W - len(err_text) - 2)}{B}│{RESET}")
        # 호출 위치 링크
        link = f"{filename}:{lineno}"
        link_display = f"  → {CYAN}{UNDERLINE}{link}{RESET}"
        visible_len = 4 + len(link)
        print(f"{B}│{RESET}{link_display}{' ' * (W - visible_len)}{B}│{RESET}")
        print(f"{B}└{'─' * W}┘{RESET}")
        self._print_task_table()

    def get_signal_client(self, client_id: str = "_external") -> 'SignalClient':
        """외부 Signal 수신용 SignalClient 생성"""
        if not self._signal_broker:
            raise RuntimeError("SignalBroker not initialized")
        return SignalClient(self._signal_broker, client_id)

    def get_smblock(self, name: str) -> 'SmBlock':
        """SmBlock 인스턴스 반환"""
        pool = SmBlockHandler._pools.get(name)
        if not pool:
            available = list(SmBlockHandler._pools.keys())
            smblock_not_found_error(name, available)
            raise SmBlockNotFoundError(name, available)
        return pool

    def get_smblock_names(self) -> list:
        """등록된 모든 SmBlock 이름 반환"""
        return list(SmBlockHandler._pools.keys())

    def get_buffer(self, smblock_name: str, copy: bool = False):
        """SmBlock 버퍼 컨텍스트 매니저 (with문으로 자동 할당/해제)"""
        return self.get_smblock(smblock_name).buffer(copy=copy)

    def _pre_subscribe_signals(self, task_id: str, job_class: type):
        """Process 모드 Task의 시그널 구독을 메인 브로커에 사전 등록

        Process 모드 Task는 subprocess에서 실행되며, pickle된 broker 사본을 사용.
        subprocess에서 broker.subscribe()해도 main broker의 _sub_snapshot에 반영되지 않아
        relay를 통한 시그널 분배가 실패함. 이를 방지하기 위해 main broker에 미리 등록.
        """
        broker = self._signal_broker
        # 1. @rmi_signal 데코레이터
        for name in dir(job_class):
            if name.startswith('__'):
                continue
            method = getattr(job_class, name, None)
            if callable(method) and hasattr(method, '_signal_handler'):
                broker.subscribe(method._signal_handler, task_id)
        # 2. signal_forward (RMI Signal → Qt Signal)
        forward = getattr(job_class, '_rmi_signal_forward', {}) or {}
        for signal_name in forward:
            broker.subscribe(signal_name, task_id)
        # 3. _tc_signal_subscribe (on_* 자동 감지 + 명시적 목록)
        subscribe_list = getattr(job_class, '_tc_signal_subscribe', []) or []
        for signal_name in subscribe_list:
            broker.subscribe(signal_name, task_id)

    def _print_task_table(self):
        headers = ['ID', 'Mode', 'Class', 'rmi_name', 'rmi_run']
        rows = []
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            rmi_name = getattr(cls, '_tc_name', '-') if cls else '-'
            rmi_run = self._find_rmi_run_method(cls) if cls else '-'
            mode_str = ti.mode + ('(M)' if ti.main_thread else '')  # M = main_thread
            rows.append([tid, mode_str, ti.task_class_name, rmi_name, rmi_run])
        widths = [max(len(h), max((len(str(r[i])) for r in rows), default=0)) for i, h in enumerate(headers)]
        top    = '┌' + '┬'.join('─' * (w + 2) for w in widths) + '┐'
        mid    = '├' + '┼'.join('─' * (w + 2) for w in widths) + '┤'
        bottom = '└' + '┴'.join('─' * (w + 2) for w in widths) + '┘'
        print('\n' + top)
        print('│' + '│'.join(f' {h:<{w}} ' for h, w in zip(headers, widths)) + '│')
        print(mid)
        for row in rows:
            print('│' + '│'.join(f' {str(c):<{w}} ' for c, w in zip(row, widths)) + '│')
        print(bottom + '\n')

    def _find_rmi_run_method(self, cls: type) -> str:
        for name in dir(cls):
            if name.startswith('__'): continue
            method = getattr(cls, name, None)
            if callable(method) and has_run_flag(method):
                return name
        if hasattr(cls, 'run') and callable(getattr(cls, 'run', None)):
            return 'run'
        if hasattr(cls, 'rmi_loop') and callable(getattr(cls, 'rmi_loop', None)):
            return 'rmi_loop'
        return '-'

    def __enter__(self): self.start_all(); return self
    def __exit__(self, *_): self.stop_all(); return False


# ──────────────────────────────────────────────────────────────────────────────
# TaskRuntime
# ──────────────────────────────────────────────────────────────────────────────

class TaskRuntime:
    __slots__ = ('ti', 'tasks', 'job', '_run', '_th', '_measurement', '_timing_recorder',
                 '_signal_broker', '_signal_client', '_rmi_timeout', 'log',
                 '_debug_signal', '_debug_method', '_debug_variable', '_gconfig', '_manager')

    def __init__(self, ti: TaskInfo, tasks: Dict[str, TaskInfo], measurement=None, signal_broker=None, rmi_timeout: float = _DEFAULT_RMI_TIMEOUT, gconfig=None, manager=None):
        self.ti, self.tasks, self.job, self._run, self._th = ti, tasks, None, True, None
        self._measurement = measurement
        self._rmi_timeout = rmi_timeout
        self._timing_recorder = None  # run()에서 초기화 (pickle 호환)
        self._signal_broker = signal_broker
        self._signal_client = None
        self.log = None  # run()에서 초기화 (pickle 호환)
        self._debug_signal = False  # run()에서 설정
        self._debug_method = False  # run()에서 설정
        self._debug_variable = False  # run()에서 설정
        self._gconfig = gconfig
        self._manager = manager

    @property
    def manager(self):
        """TaskManager 인스턴스 반환 (Thread 모드 전용)"""
        if self.ti.mode != 'thread':
            return None
        return self._manager

    @property
    def gconfig(self):
        """Global configuration 반환"""
        return self._gconfig

    def __getstate__(self):
        """Pickle 직렬화: unpicklable 객체 제외"""
        return {
            'ti': self.ti,
            'tasks': self.tasks,
            'job': None,
            '_run': self._run,
            '_th': None,
            '_measurement': self._measurement,
            '_timing_recorder': None,
            '_signal_broker': self._signal_broker,
            '_signal_client': None,
            '_rmi_timeout': self._rmi_timeout,
            'log': None,
            '_debug_signal': False,
            '_debug_method': False,
            '_debug_variable': False,
            '_gconfig': self._gconfig,
        }

    def __setstate__(self, state):
        """Pickle 역직렬화"""
        self.ti = state['ti']
        self.tasks = state['tasks']
        self.job = state['job']
        self._run = state['_run']
        self._th = state['_th']
        self._measurement = state['_measurement']
        self._timing_recorder = state['_timing_recorder']
        self._signal_broker = state['_signal_broker']
        self._signal_client = state['_signal_client']
        self._rmi_timeout = state['_rmi_timeout']
        self.log = state['log']
        self._debug_signal = state.get('_debug_signal', False)
        self._debug_method = state.get('_debug_method', False)
        self._debug_variable = state.get('_debug_variable', False)
        self._gconfig = state.get('_gconfig')
        self._manager = None

    def run(self):
        ti = self.ti
        # pickle 후 재초기화 (process 모드)
        if self._timing_recorder is None:
            self._timing_recorder = TimingRecorder(ti.rmi_methods, ti.rmi_timing)
        if self.log is None:
            from .task_log import RmiLogger
            self.log = RmiLogger(self)
        if not ti.job_class:
            print(f"[W:{ti.task_id}] no job_class"); return
        # main_thread=True인 경우 메인 스레드에서 생성된 인스턴스 사용 (QObject 지원)
        if ti.job_instance is not None:
            self.job = ti.job_instance
        else:
            self.job = ti.job_class()  # __init__(self) 호출
            self.job._input_queue = queue.Queue()  # 기본 입력 큐 주입
            # on_input 메서드 자동 생성 (미정의 시)
            if not hasattr(self.job, 'on_input'):
                self.job.on_input = lambda data: self.job._input_queue.put_nowait(data)
            # Thread 모드: DirectClient가 참조할 수 있도록 저장
            if ti.mode == 'thread':
                ti.job_instance = self.job
        self.job.runtime = self   # runtime 속성 주입
        
        # Device Property 초기화
        if hasattr(self.job, "_device_init"):
            self.job._device_init()

        # debug 파싱: True/False 또는 "signal,method,variable" 문자열
        debug_cfg = getattr(ti.job_class, '_tc_debug', False)
        if debug_cfg is True:
            self._debug_signal = self._debug_method = self._debug_variable = True
        elif isinstance(debug_cfg, str):
            opts = {o.strip().lower() for o in debug_cfg.split(',')}
            self._debug_signal = 'signal' in opts
            self._debug_method = 'method' in opts or 'ipc' in opts or 'rmi' in opts
            self._debug_variable = 'variable' in opts or 'var' in opts
        if self._signal_broker:
            # v2.4: 서브프로세스에서 SmSignalRegistry 재연결
            if ti.mode == 'process' and self._signal_broker._sm_registry is None:
                reg_name = getattr(self._signal_broker, '_sm_registry_name', None)
                if reg_name:
                    try:
                        self._signal_broker._sm_registry = SmSignalRegistry(name=reg_name, create=False)
                    except Exception:
                        pass
            # v2.4: notify_event 조회/재연결
            _notify_ev = self._signal_broker._notify_events.get(ti.task_id)
            if _notify_ev is None and ti.mode == 'process':
                # 서브프로세스: SmKernelEvent re-open (Win32) 또는 pickle된 mp.Event
                if _IS_WIN32:
                    _sid = ti.task_id.replace('.', '_').replace('-', '_')
                    _evt_name = (getattr(self._signal_broker, '_sm_registry_name', '') or '').replace('alaska_sig_reg_', '')
                    if _evt_name:
                        try:
                            from ..sm_infra import SmKernelEvent
                            _notify_ev = SmKernelEvent(f"al_notify_{_evt_name}_{_sid}", create=False, manual_reset=True)
                        except Exception:
                            _notify_ev = None
            # v2.6: response_event 재연결 (process 모드 — ctypes SmKernelEvent pickle 불가)
            #        자신 + 다른 모든 태스크의 response_event를 재연결 (중첩호출 시 caller 깨움 필요)
            if ti.mode == 'process' and _IS_WIN32:
                _pid_str = (getattr(self._signal_broker, '_sm_registry_name', '') or '').replace('alaska_sig_reg_', '')
                if _pid_str:
                    from ..sm_infra import SmKernelEvent
                    for _tid, _tinfo in self.tasks.items():
                        if _tinfo.response_event is not None:
                            continue
                        _s = _tid.replace('.', '_').replace('-', '_')
                        try:
                            _tinfo.response_event = SmKernelEvent(f"al_rmi_resp_{_pid_str}_{_s}", create=False, manual_reset=True)
                        except Exception:
                            _tinfo.response_event = None
            self._signal_client = SignalClient(self._signal_broker, ti.task_id, mode=ti.mode,
                                               debug=self._debug_signal, notify_event=_notify_ev)
            self._signal_client._job = self.job
            # ★ start() 호출 안 함 — InvokeDispenser가 Signal IO 스레드 대체
            self._auto_subscribe_signals()
            self.job.signal = _SignalEmitter(self._signal_client)  # signal 체인 접근자 주입
        # ★ InvokeDispenser 시작 (RMI + Signal 통합, 태스크당 2스레드)
        self._th = threading.Thread(target=self._invoke_dispenser, daemon=True)
        self._th.start()
        self._inject_dependencies()
        if not self._validate_injection():
            if self._signal_client: self._signal_client.stop()
            if ti._ready_event: ti._ready_event.set()
            return
        # 초기화 완료 신호 (동적 생성 시 add_task()가 대기 중)
        if ti._ready_event:
            ti._ready_event.set()
        self._run_main_loop()
        # ★ InvokeDispenser 정지
        self._run = False
        if self._signal_client and self._signal_client._notify_event:
            try: self._signal_client._notify_event.set()  # 커널 대기 깨움
            except Exception: pass
        if self._th:
            self._th.join(timeout=1.0)
        if self._signal_client:
            self._signal_client.off_all()
            self._signal_client.stop()

    def _validate_injection(self) -> bool:
        ti, job = self.ti, self.job
        errors = []
        if 'smblock' in ti.injection and (not hasattr(job, 'smblock') or job.smblock is None):
            errors.append("smblock not injected")
        if 'next' in ti.injection and (not hasattr(job, 'next') or job.next is None):
            errors.append("next not injected")
        if errors:
            print(f"[W:{ti.task_id}] ERROR: {', '.join(errors)}")
            time.sleep(3)
            return False
        return True

    def _auto_subscribe_signals(self):
        if not self._signal_client or not self.job: return
        registered = {}  # {signal_name: [handler_info, ...]}

        # @rmi_signal 데코레이터 처리
        for signal_name, method in get_signal_handlers(self.job):
            self._signal_client.on(signal_name, method)
            registered.setdefault(signal_name, []).append(f"@rmi_signal:{method.__name__}")

        # _rmi_signal_forward 선언적 매핑 처리 (RMI Signal → Qt Signal)
        # @rmi_signal과 동일 시그널이어도 BOTH 핸들러 등록 (둘 다 실행)
        bridge = get_rmi_signal_forward(self.job)
        for rmi_signal_name, qt_signal_attr in bridge.items():
            qt_signal = getattr(self.job, qt_signal_attr, None)
            if qt_signal is not None and hasattr(qt_signal, 'emit'):
                def make_handler(qs, attr_name):
                    def handler(sig):
                        qs.emit(sig.data)
                    handler.__name__ = f"bridge:{attr_name}"
                    return handler
                self._signal_client.on(rmi_signal_name, make_handler(qt_signal, qt_signal_attr))
                registered.setdefault(rmi_signal_name, []).append(f"signal_forward:{qt_signal_attr}")

        # signal_subscribe: 시그널 이름 규칙 기반 자동 핸들러 매핑
        # signal.{path}.emit(data) → on_{path_with_underscore}(data)
        # 예: "measurement.start" → on_measurement_start(data)
        subscribe_list = getattr(self.job.__class__, '_tc_signal_subscribe', []) or []
        for signal_name in subscribe_list:
            if signal_name in registered:
                continue  # 이미 @rmi_signal 또는 signal_forward로 등록됨
            # 메서드 이름 변환: measurement.start → on_measurement_start
            method_name = "on_" + signal_name.replace(".", "_")
            handler = getattr(self.job, method_name, None)
            if handler and callable(handler):
                self._signal_client.on(signal_name, handler)
                registered.setdefault(signal_name, []).append(f"signal_subscribe:{method_name}")
            else:
                # 핸들러 없으면 구독만 (브로커에 등록)
                self._signal_client._broker.subscribe(signal_name, self.ti.task_id)
                registered.setdefault(signal_name, []).append("signal_subscribe(no handler)")

        # 등록된 핸들러 로그 출력
        if registered:
            print(f"[{self.ti.task_id}] Signal subscriptions: {list(registered.keys())}")
        for sig_name, handlers in registered.items():
            if len(handlers) > 1:
                print(f"[{self.ti.task_id}] Signal '{sig_name}' handlers: {handlers} (BOTH will execute)")

    def _invoke_dispenser(self):
        """InvokeDispenser v2.6: 단일 스레드 통합 디스패처 (IPC/RMI + Signal)

        설계서: doc/2016.InvokeDispenser설계.txt

        루프 순서:
          ① Signal RECV (QoS P1→P5) — ★ 우선
          ② IPC/RMI (request_q, 5건/cycle) — Signal 후순
          ③ Kernel Wait (idle 시 CPU 0%)
        SEND는 v2.6에서 제거 — emit()이 caller 스레드에서 직접 처리
        """
        ti, job, tasks = self.ti, self.job, self.tasks
        sc = self._signal_client
        task_id = ti.task_id

        # ── 입력 소스 ──
        rb = ti.request_q                     # SmRingBuffer: RMI 요청
        stop = ti.stop_flag
        qs = sc._queues                       # QoS P1~P5 (5-tuple)
        scheduler = sc._scheduler
        notify_event = sc._notify_event
        broker = sc._broker

        # v2.6: is_sleeping 플래그 (CPU 절감)
        _is_sleeping = ti.is_sleeping

        # ── 로컬 바인딩 (LOP-1) ──
        _rb_read = rb.read
        _rb_used = rb._used
        _pickle_loads = pickle.loads
        _pickle_dumps = pickle.dumps
        _tasks_get = tasks.get
        _MISSING_REF = _MISSING
        _method_cache = {}
        # ★ v2.6: Method Cache Pre-warm (첫 호출 getattr 제거)
        for _m_name in dir(job):
            if _m_name.startswith('_'): continue
            try:
                _m_ref = getattr(job, _m_name, _MISSING)
                if callable(_m_ref):
                    _method_cache[_m_name] = _m_ref
            except (RuntimeError, AttributeError):
                continue
        _callable = callable
        _getattr = getattr
        _setattr = setattr
        _vars = vars
        _isinstance = isinstance
        _type = type
        _time = time.time
        _queue_Empty = queue.Empty

        # ── 상수 ──
        _RMI_BATCH = 5
        _BURST_LIMIT = 50            # v2.6: 카운트 기반 burst guard (perf_counter 제거)
        _NOTIFY_TIMEOUT = 0.1         # 100ms
        _STOP_CHECK_LIMIT = 100       # cycles

        # ── 상태 ──
        _stop_local = False
        _cycle_count = 0
        _HB_CYCLES = 500            # v2.6: 카운트 기반 하트비트 (~500 cycles ≈ 1초)
        _hb_cnt = 0

        def _check_stop():
            nonlocal _stop_local
            try: _stop_local = stop.value
            except Exception: _stop_local = True

        while self._run and not _stop_local:
            any_work = False

            # ★ v2.6: 하트비트 갱신 (카운트 기반 — perf_counter 제거)
            _hb_cnt += 1
            if _hb_cnt >= _HB_CYCLES:
                _hb_cnt = 0
                try: broker._heartbeats[task_id] = _time()
                except Exception: pass

            # ═══════════════════════════════════════════════
            # ① Signal RECV — QoS P1→P5 (★ 우선)
            #    v2.6: 카운트 기반 burst guard (perf_counter 제거)
            # ═══════════════════════════════════════════════
            sc._cycle_time = _time()  # v2.6: cycle-time (deadline 체크용)
            scheduler.new_cycle()
            _burst_count = 0
            for p in range(5):
                quota = scheduler.CYCLE_QUOTAS[p]
                for _ in range(quota):
                    try:
                        data = qs[p].get_nowait()
                    except (_queue_Empty, IndexError):
                        break
                    sc._dispatch(data)
                    scheduler.consume(p)
                    any_work = True
                    _burst_count += 1
                    if _burst_count >= _BURST_LIMIT:
                        break
                if _burst_count >= _BURST_LIMIT:
                    break

            # ═══════════════════════════════════════════════
            # ② IPC/RMI — request_q (Signal 후순)
            # ═══════════════════════════════════════════════
            for _ in range(_RMI_BATCH):
                data = _rb_read()
                if data is None:
                    break

                req = _pickle_loads(data)
                m, a, kw = req['m'], req.get('a', ()), req.get('kw', {})
                _req_id = req.get('_id')
                caller_id = req.get('c')
                chain = req.get('chain', set())
                rq = req.get('rq')

                _caller_resp_evt = None
                if caller_id:
                    caller_ti = _tasks_get(caller_id)
                    if caller_ti:
                        rq = caller_ti.response_q
                        _caller_resp_evt = caller_ti.response_event  # v2.6

                # ★ 순환 호출 감지
                if task_id in chain:
                    if rq:
                        try: rq.write(_pickle_dumps({'e': "Circular RMI"}, protocol=5))
                        except Exception: pass
                        if _caller_resp_evt is not None:
                            try: _caller_resp_evt.set()
                            except Exception: pass
                    continue

                # Method Dispatch (기존 _rmi_handler 로직 동일)
                try:
                    if m[0] != '_':
                        fn = _method_cache.get(m)
                        if fn is None:
                            fn = _getattr(job, m, _MISSING_REF)
                            if fn is not _MISSING_REF:
                                _method_cache[m] = fn
                        if fn is _MISSING_REF:
                            res = {'e': f"Method '{m}' not found"}
                        else:
                            res = {'r': fn(*a, **kw) if _callable(fn) else fn}
                    elif m == '__getvar__':
                        val = _getattr(job, a[0], _MISSING_REF)
                        res = {'e': "Attr not found"} if val is _MISSING_REF else {'r': val() if _callable(val) else val}
                    elif m == '__setvar__':
                        _setattr(job, a[0], a[1]); res = {'r': True}
                    elif m == '__set_config__':
                        _setattr(job, a[0], a[1]); res = {'r': True}
                    elif m == '__get_debug__':
                        res = {'r': {'signal': self._debug_signal, 'method': self._debug_method, 'variable': self._debug_variable}}
                    elif m == '__set_debug__':
                        flag, value = a[0], bool(a[1])
                        if flag == 'signal': self._debug_signal = value
                        elif flag == 'method': self._debug_method = value
                        elif flag == 'variable': self._debug_variable = value
                        res = {'r': True}
                    elif m == '__list_vars__':
                        _SYS = {'running', 'task_name', 'runtime', 'signal'}
                        result = []
                        for name in sorted(_vars(job)):
                            if name.startswith('_') or name in _SYS: continue
                            val = _getattr(job, name, None)
                            if _callable(val): continue
                            vtype = _type(val).__name__
                            editable = _isinstance(val, (int, float, str, bool))
                            result.append({'name': name, 'type': vtype, 'value': val if editable else f"<{vtype}>", 'editable': editable})
                        res = {'r': result}
                    else:
                        res = {'e': "Unknown internal method"}
                except Exception as e:
                    import traceback as _tb
                    res = {'e': str(e), 'trace': _tb.format_exc()}

                # Response 전송 (v2.6: _id 복사 — stale 응답 감지용)
                if _req_id is not None:
                    res['_id'] = _req_id
                if rq:
                    try:
                        if hasattr(rq, 'write'):
                            resp_data = _pickle_dumps(res, protocol=5)
                            for _ in range(_MAX_WRITE_RETRY):
                                if rq.write(resp_data):
                                    break
                                time.sleep(0)
                        else:
                            rq.put(res)
                    except Exception: pass
                    # ★ v2.6: Caller의 response_event 깨움 (spin-wait → kernel wait)
                    if _caller_resp_evt is not None:
                        try: _caller_resp_evt.set()
                        except Exception: pass

                any_work = True

            # ═══════════════════════════════════════════════
            # ③ Kernel Wait — idle 시 CPU 0%
            #    v2.6: is_sleeping 플래그로 조건부 notify 지원
            # ═══════════════════════════════════════════════
            if not any_work:
                # ★ 잠들기 전 플래그 설정 (송신측이 읽고 조건부 set)
                if _is_sleeping is not None:
                    try: _is_sleeping.value = 1
                    except Exception: pass
                if notify_event is not None:
                    try: notify_event.clear()
                    except Exception: pass

                # Double-Check (Race Condition 방지)
                still_idle = True
                for p in range(5):
                    if not qs[p].empty():
                        still_idle = False; break
                if still_idle and _rb_used() > 0:
                    still_idle = False

                if still_idle:
                    if notify_event is not None:
                        try: notify_event.wait(timeout=_NOTIFY_TIMEOUT)
                        except Exception: time.sleep(0.001)
                    else:
                        time.sleep(0.001)
                # ★ 깨어남 — 플래그 해제
                if _is_sleeping is not None:
                    try: _is_sleeping.value = 0
                    except Exception: pass

            # Stop flag 주기적 확인
            _cycle_count += 1
            if _cycle_count >= _STOP_CHECK_LIMIT:
                _check_stop()
                _cycle_count = 0


    def _inject_dependencies(self):
        job, tasks, ti = self.job, self.tasks, self.ti
        SmBlockHandler.injection(self, tasks)
        for k, v in ti.injection.items():
            if isinstance(v, dict) and "_smblock" in v: continue
            if isinstance(v, str) and v.startswith("client:"):
                # client:task_id → RmiClient 프록시 주입
                target_id = v[7:]
                t = tasks.get(target_id)
                if t:
                    setattr(job, k, self._create_rmi_client(target_id, t))
            elif isinstance(v, str) and v.startswith("task:"):
                # task:task_id → 실제 인스턴스 주입 (thread 모드 전용)
                target_id = v[5:]
                t = tasks.get(target_id)
                if t and t.job_instance:
                    setattr(job, k, t.job_instance)
                elif t:
                    # job_instance 없으면 RmiClient로 fallback
                    print(f"[W:{ti.task_id}] task:{target_id} has no instance, using RmiClient")
                    setattr(job, k, self._create_rmi_client(target_id, t))
            else:
                setattr(job, k, v)

    def _run_main_loop(self):
        ti, job = self.ti, self.job

        # @rmi_run 데코레이터 탐지
        loop_method = None
        try:
            for name in dir(job):
                if name.startswith('__'): continue
                try:
                    method = getattr(job, name, None)
                    if callable(method) and has_run_flag(method):
                        loop_method = method
                        break
                except (RuntimeError, AttributeError):
                    continue
        except (RuntimeError, AttributeError):
            pass

        # Fallback: run() 또는 rmi_loop()
        if loop_method is None:
            loop_method = getattr(job, 'run', None) or getattr(job, 'rmi_loop', None)

        if not loop_method:
            # QWidget/QObject (main_thread=True)는 run() 없이도 시그널 수신 대기
            if ti.main_thread:
                while self._run and not ti.stop_flag.value:
                    time.sleep(0.1)
                return
            return
        while self._run:
            try:
                loop_method()  # run(self) - self.runtime으로 접근
                # main_thread=True (QWidget 등)는 run() 완료 후에도 시그널 수신 대기
                if ti.main_thread:
                    while self._run and not ti.stop_flag.value:
                        time.sleep(0.1)
                break
            except Exception as e:
                print(f"[W:{ti.task_id}] {loop_method.__name__}: {e}")
                traceback.print_exc()
                if not ti.auto_restart: break
                ti.restart_cnt.value += 1
                time.sleep(ti.restart_delay)

    def should_stop(self) -> bool:
        try:
            return not self._run or self.ti.stop_flag.value
        except Exception:
            return True # Manager가 닫혔거나 통신 오류 시 정지 유도

    @property
    def running(self) -> bool:
        """Task가 실행 중인지 확인 (should_stop()의 반대)

        Example:
            # Before
            while not self.runtime.should_stop():
                pass

            # After (더 자연스러움)
            while self.running:
                pass
        """
        return not self.should_stop()

    @property
    def name(self) -> str:
        return self.ti.task_id

    def _create_rmi_client(self, task_id: str, target_info: TaskInfo):
        """RMI 클라이언트 생성 헬퍼
        - Thread-Thread: DirectClient (IPC 없음, 최고 성능)
        - 그 외: RmiClient + Queue (단일 채널)
        """
        ti = self.ti
        caller_mode = ti.mode
        target_mode = target_info.mode

        # Thread-Thread: 직접 호출 (IPC 없음)
        if caller_mode == 'thread' and target_mode == 'thread':
            tasks = self.tasks
            def job_getter():
                # Lazy loading: target task의 job 인스턴스 반환
                # thread 모드에서는 같은 프로세스이므로 직접 접근 가능
                target_ti = tasks.get(task_id)
                if target_ti and target_ti.job_instance:
                    return target_ti.job_instance
                # job_instance가 없으면 runtimes에서 찾기 시도
                return None
            return DirectClient(task_id, job_getter, caller=ti.task_id,
                                debug_method=self._debug_method,
                                debug_variable=self._debug_variable,
                                broker=self._signal_broker)

        # ★ Target의 notify_event 조회 (InvokeDispenser 깨움용)
        _ne = None
        if self._signal_broker:
            _ne = self._signal_broker._notify_events.get(task_id)
            if _ne is None:
                _ne = self._signal_broker._notify_cache.get(task_id)
            if _ne is None and _IS_WIN32:
                ne_name = self._signal_broker._notify_event_names.get(task_id)
                if ne_name:
                    try:
                        from ..sm_infra import SmKernelEvent
                        _ne = SmKernelEvent(ne_name, create=False, manual_reset=True)
                    except Exception:
                        _ne = None

        # SmRingBuffer 단일 채널 (Lock-free SPSC)
        return RmiClient(
            task_id, target_info.request_q, ti.response_q, timeout=self._rmi_timeout,
            request_lock=target_info.request_lock,
            tx_counter=ti.rmi_tx, time_counter=ti.rmi_time,
            time_min=ti.rmi_time_min, time_max=ti.rmi_time_max,
            caller=ti.task_id,
            debug_method=self._debug_method,
            debug_variable=self._debug_variable,
            broker=self._signal_broker,
            notify_event=_ne,
            response_event=ti.response_event,               # v2.6: caller의 응답 이벤트
            target_sleeping=target_info.is_sleeping          # v2.6: target의 is_sleeping 플래그
        )

    def get_client(self, task_id: str) -> RmiClient:
        t = self.tasks.get(task_id)
        if not t: raise ValueError(f"Task '{task_id}' not found")
        return self._create_rmi_client(task_id, t)

    def restart_task(self, task_id: str):
        """특정 태스크를 중지하고 잔여 데이터 청소 후 재시작 (v2.3 안정성 강화)"""
        with self._task_lock:
            ti = self._tasks.get(task_id)
            worker = self._workers.get(task_id)
            if not ti or not worker: return

            print(f"[TaskManager] Restarting task: {task_id}")

            # 1. 기존 프로세스/스레드 정지
            if ti.mode == 'process':
                worker.terminate()
                worker.join(timeout=1.0)
                if worker.is_alive(): worker.kill()
            else:
                # Thread 모드는 플래그로 정지 유도
                ti.stop_flag.value = True
                if isinstance(worker, threading.Thread):
                    worker.join(timeout=1.0)

            # 2. 잔여 큐 데이터 청소 (v2.3: 멀티큐 대응)
            if self._signal_broker:
                try:
                    qs = self._signal_broker._queues.get(task_id)
                    if qs:
                        for q in qs:
                            while not q.empty():
                                try: q.get_nowait()
                                except: break
                except Exception: pass

            # 2b. SmRingBuffer 인덱스 초기화
            for q in (ti.request_q, ti.response_q):
                if q and hasattr(q, 'reset'):
                    try: q.reset()
                    except Exception: pass

            # 3. 상태 초기화 및 재시작
            ti.stop_flag.value = False
            ti.restart_cnt.value += 1
            
            # Runtime 및 Worker 재성성 로직 (start_task와 유사)
            runtime = TaskRuntime(ti, self._tasks, 
                                  measurement=self._measurement,
                                  signal_broker=self._signal_broker, 
                                  rmi_timeout=self._rmi_timeout,
                                  gconfig=self._gconfig,
                                  manager=self)
            
            if ti.mode == 'process':
                new_worker = multiprocessing.Process(target=runtime.run, daemon=True)
            else:
                new_worker = threading.Thread(target=runtime.run, daemon=True)
            
            self._workers[task_id] = new_worker
            self._runtimes[task_id] = runtime
            new_worker.start()
            print(f"[TaskManager] Task '{task_id}' restarted successfully.")

    @property
    def signal(self) -> SignalClient:
        return self._signal_client
