
    Pmj-B                       U d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ  ej        e          ZdZdZ ej                    Zi Zded	<    G d
 de          Zd;dZd<dZd=dZd=dZd>dZ G d d          Z G d deej                  Zi Zded<   d?d"Z ddd#d@d&Z!dAd(Z"dBd*Z#dCd,Z$d-d.d/dDd4Z% G d5 d6e          Z&ej'        d7d8dEd:            Z(dS )Fu  Lock-safe inspection of SQLite database files.

Why this module exists
----------------------
POSIX advisory locks are cancelled **process-wide** by ``close()`` on *any*
file descriptor for that file::

    the close() system call will cancel all POSIX advisory locks on the
    same file for all threads and all file descriptors in the process
    -- https://sqlite.org/howtocorrupt.html#_posix_advisory_locks_canceled_by_a_separate_thread_doing_close_

So a bare ``open(db_path, "rb") ... close()`` on a **live** database silently
drops every lock SQLite holds on it from this process -- including the
EXCLUSIVE lock a ``VACUUM`` is holding while it rewrites the whole file, and
the RESERVED lock an in-flight ``BEGIN IMMEDIATE`` is holding. Other processes
are then free to write into a file that a writer still believes it owns, which
is the documented route to "database disk image is malformed".

Hermes is exactly the topology this hits: gateway, dispatcher, dashboard,
TUI, CLI, cron and kanban workers all open the same ``state.db`` /
``kanban.db``, and several code paths used to byte-probe those files while
connections were live.

The rules
---------
1. **Never** ``open()`` a database file that may have live connections in this
   process. Ask SQLite instead -- :func:`page_count_bytes` reads the same
   header field via ``PRAGMA``, over the existing connection, taking no new
   descriptor.
2. Byte-level probes are only safe **before any connection exists** for that
   path (first-open validation). Route those through
   :func:`read_header_bytes_preopen`, which refuses once a connection has been
   registered for the path.

Concurrency contract
--------------------
The registry is not advisory bookkeeping -- it is the guard, so the
check and the byte read must be **atomic with respect to connection
lifecycle**. ``_live_lock`` is therefore held across three critical sections,
each of which spans the syscall *and* the registry mutation:

* open + register (:func:`connect_tracked`)
* close + unregister (:meth:`TrackedConnection.close`)
* check + ``open``/``read``/``close`` (:func:`read_header_bytes_preopen`)

Without that, a thread could pass the "no live connection" check, a second
thread could open a connection and take a write lock, and the first thread's
``close()`` would then cancel it -- reintroducing the exact bug this module
exists to prevent. The lock is never held while a caller *uses* a connection,
only across these transitions, so it does not serialise database work.

Path identity
-------------
Connections are keyed by the **canonical database path**, resolved from
``PRAGMA database_list`` on the opened connection. The caller's spelling is
not trustworthy: ``SessionDB``'s read-only path opens
``file:/…/state.db?mode=ro`` with ``uri=True``, and treating that string as a
filesystem path yields a key like ``<cwd>/file:/…/state.db?mode=ro`` which no
later probe of the real ``Path`` can ever match.
    )annotationsN)Path)Optionals   SQLite format 3    zdict[str, int]_live_connectionsc                      e Zd ZdZdS )UntrackableConnectionErrorzA connection to a probe-able database could not be tracked.

    Raised rather than silently returning an untracked connection: on these
    paths tracking is part of the correctness contract, not an optimisation.
    N__name__
__module____qualname____doc__     A/home/thesage/.hermes/hermes-agent/hermes_cli/sqlite_safe_read.pyr	   r	   V   s           r   r	   path
Path | strreturnstrc                    	 t          t          |                                                     S # t          $ r t          |           cY S w xY w)z;Canonicalise a *filesystem* path for use as a registry key.)r   r   resolveOSErrorr   s    r   _keyr   ^   sT    4::%%''(((   4yys   -0 AAconnsqlite3.ConnectionOptional[str]c                    	 |                      d                                          }n# t          j        $ r Y dS w xY w|rt	          |          dk     rdS |d         }|sdS t          |          S )a	  The on-disk path of ``main``, as SQLite itself reports it.

    Immune to the caller's spelling (``file:`` URIs, relative paths, symlinks).
    Returns ``None`` for in-memory or unnamed databases, which cannot be
    byte-probed and therefore need no tracking.
    zPRAGMA database_listN      )executefetchonesqlite3Errorlenr   )r   rowpath_strs      r   _canonical_db_pathr(   f   s    ll122;;===   tt #c((Q,,t1vH t>>s   '* ==Nonec                    t          |           }t          5  t                              |d          dz   t          |<   ddd           dS # 1 swxY w Y   dS )zRecord that this process now holds a connection to *path*.

    Prefer :func:`connect_tracked`; this exists for callers that manage their
    own connection objects, and for tests.
    r      N)r   
_live_lockr   get)r   keys     r   track_connectionr/   y   s     t**C	 C C!2!6!6sA!>!>!B#C C C C C C C C C C C C C C C C C Cs   'AAAc                    t          |           }t          5  t                              |d          dz
  }|dk    r|t          |<   nt                              |d           ddd           dS # 1 swxY w Y   dS )z5Record that one connection to *path* has been closed.r   r+   N)r   r,   r   r-   pop)r   r.   	remainings      r   untrack_connectionr3      s    
t**C	 - -%))#q11A5	q==%.c""!!#t,,,- - - - - - - - - - - - - - - - - -s   AA//A36A3boolc                p    t           5  t          |           t          v cddd           S # 1 swxY w Y   dS )z>Whether this process currently holds any connection to *path*.N)r,   r   r   r   s    r   has_live_connectionr6      s{    	 / /Dzz../ / / / / / / / / / / / / / / / / /s   +//c                  4     e Zd ZU dZdZded<   d fdZ xZS )	_TrackingMixinzAUntrack-on-close behaviour, mixable into any Connection subclass.Nz
str | None_hermes_tracked_pathr   r)   c                    t           5  t          | dd           }t                                                       |d | _        t          |           d d d            d S # 1 swxY w Y   d S )Nr9   )r,   getattrsupercloser9   r3   )selfr   	__class__s     r   r=   z_TrackingMixin.close   s     		) 		)4!7>>D
 GGMMOOO,0)"4(((		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		) 		)s   A
A  A$'A$)r   r)   )r   r   r   r   r9   __annotations__r=   __classcell__)r?   s   @r   r8   r8      sW         KK'+++++
) 
) 
) 
) 
) 
) 
) 
) 
) 
)r   r8   c                      e Zd ZdZdS )TrackedConnectionu  A ``sqlite3.Connection`` that untracks its path exactly once on close.

    Counting opens is easy; counting closes reliably is not, because callers
    close connections in many places (and some hand them to
    ``contextlib.closing``). Putting the decrement on ``close()`` — the one
    method every close path must go through — keeps the registry from
    drifting upward and permanently disabling byte-probes.

    The real ``close()`` and the unregister happen together under
    ``_live_lock`` so a concurrent probe can never observe "no live
    connection" while this descriptor is still open. Unregister runs only
    after ``close()`` succeeds; a raising close leaves the connection
    tracked so the byte-probe guard keeps refusing.

    Note ``with conn:`` does NOT close a sqlite3 connection (it only commits or
    rolls back), so this hook is not fired spuriously by transaction scopes.
    Nr
   r   r   r   rC   rC      s           r   rC   zdict[type, type]_tracked_factory_cachefactorytypec                    | t           j        u rt          S t          | t                    r| S t
                              |           }|*t          d| j         t          | fi           }|t
          | <   |S )u  Return *factory* augmented with untrack-on-close.

    Callers legitimately supply their own ``Connection`` subclasses (the test
    suite uses them to simulate FTS5-less or pragma-failing runtimes). Rather
    than refusing those — or silently leaving them untracked, which would
    quietly unguard the database — we mix the tracking ``close()`` into the
    caller's class so tracking is preserved either way.
    NTracked)	r#   
ConnectionrC   
issubclassr8   rD   r-   rF   r   )rE   cacheds     r   _tracking_factoryrL      sy     '$$$  '>** #''00F~2 022^W4MrRR*0w'Mr   )tracking_path
connect_fnrM   Path | str | Nonec               z   ||nt           j        }t          |                    dt           j                            |d<   t
          5   |t          |           fi |}	 |t          |          nt          |          }||cddd           S t          |t                    st          ||          }||_        t                              |d          dz   t          |<   |cddd           S # t          $ r3 	 t           j                            |           n# t          $ r Y nw xY w w xY w# 1 swxY w Y   dS )u  ``sqlite3.connect`` that registers the connection for the lifetime of the fd.

    Use for any connection to a database whose file might otherwise be
    byte-probed (``state.db``, ``kanban.db``). The registration is released
    automatically on ``close()``.

    The open and the registration happen together under ``_live_lock``, so a
    concurrent :func:`read_header_bytes_preopen` cannot slip between them and
    cancel this connection's locks.

    The registry key is the canonical path reported by ``PRAGMA
    database_list`` -- not *path*, which may be a ``file:`` URI. Pass
    ``tracking_path`` to override when the caller already knows the real path.

    ``connect_fn`` lets a caller supply its own opener (defaults to
    :func:`sqlite3.connect`), so a module that owns the connection — and any
    test that patches that module's ``sqlite3.connect`` — keeps control of how
    the connection is created while this helper owns tracking.

    A caller-supplied ``factory`` is honoured but is transparently augmented
    with untrack-on-close, so tracking is never silently skipped. If a
    file-backed connection still cannot be tracked,
    :class:`UntrackableConnectionError` is raised rather than handing back a
    connection whose database has quietly lost byte-probe protection.
    NrE   r   r+   )r#   connectrL   r-   rI   r,   r   r   r(   
isinstancer8   _retrofit_trackingr9   r   	Exceptionr=   )r   rM   rN   kwargsopenerr   resolveds          r   connect_trackedrX      s   @ &1ZZwF)&**Y@R*S*STTF9	  vc$ii**6**	 !, ]###'-- 
         dN33 : *$99(0D%*;*?*?!*L*Lq*Ph')       *  	 	 	 "((....   	+         sO   D0 #C0AC00
D-;DD-
D(%D-'D((D--D00D47D4rW   c                    t          |           }t          |t                    r| S 	 t          |          | _        | S # t
          $ r!}t          d| d|j         d          |d}~ww xY w)a!  Give an already-open connection untrack-on-close semantics.

    ``sqlite3.Connection`` subclasses are ordinary Python classes, so the
    instance's ``__class__`` can be swapped for one that mixes in the tracking
    ``close()``. Used when an opener ignored the factory we asked for.
    zconnection to z uses factory zn, which cannot release its tracking entry on close; byte-probe safety for this database would be silently lostN)rF   rJ   r8   rL   r?   	TypeErrorr	   r   )r   rW   clsexcs       r   rS   rS     s     t**C#~&& *3//   (7X 7 7S\ 7 7 7
 
 		s   > 
A)A$$A)Optional[int]c                   	 |                      d                                          d         }|                      d                                          d         }nE# t          j        t          t
          f$ r&}t                              d|           Y d}~dS d}~ww xY w	 t          |          t          |          z  S # t          t          f$ r Y dS w xY w)aN  Logical database size in bytes, read through *conn*.

    ``page_count * page_size`` is the same quantity the 4-byte header field at
    offset 28 carries, but reading it via ``PRAGMA`` opens no new file
    descriptor and therefore cannot cancel this process's POSIX locks.

    Returns ``None`` when the pragmas cannot be read.
    zPRAGMA page_countr   zPRAGMA page_sizez$page_count/page_size unavailable: %sN)
r!   r"   r#   r$   rZ   
IndexErrorloggerdebugint
ValueError)r   
page_count	page_sizer\   s       r   page_count_bytesrf   )  s    \\"566??AA!D
LL!344==??B		M9j1   ;SAAAttttt:Y//z"   tts*   AA B9BB#C CCOptional[bool]c                    t          |           }|dS t          |           }|sdS 	 t          j                            |          }n# t
          $ r Y dS w xY w||k    S )a  Whether the file on disk is at least as long as the header claims.

    Detects the "torn extend" shape (file shorter than its own page count)
    without ever opening the database file: the header side comes from
    ``PRAGMA page_count`` over *conn*, and the on-disk side from ``stat()``,
    which takes no descriptor and cannot break locks.

    Returns ``None`` when the check is not applicable (in-memory database,
    unreadable pragmas, or a stat failure).

    Note: in WAL mode a freshly committed page may still live in the ``-wal``
    file, so the main file legitimately lags. Callers must treat this as
    advisory unless the database is in a rollback journal mode.
    N)r(   rf   osr   getsizer   )r   r'   logicalactuals       r   file_length_matches_headerrm   >  s}     "$''Htt$$G t**   ttWs   A 
AAd   F)lengthforcero   rb   rp   Optional[bytes]c                  t           5  |s?t          |           t          v r)t                              d|            	 ddd           dS 	 t          | d          5 }|                    |          cddd           cddd           S # 1 swxY w Y   n# t          $ r Y ddd           dS w xY w	 ddd           dS # 1 swxY w Y   dS )aV  Read the first *length* bytes of *path* -- only when no connection is live.

    This is the ONLY sanctioned byte-level read of a database file, and it is
    restricted to first-open validation (is this file a real SQLite database,
    is it zeroed, has it been overwritten by something else). Once any
    connection to *path* exists in this process, the read is refused and
    ``None`` is returned, because the ``close()`` would cancel that
    connection's POSIX locks.

    The registry check and the ``open``/``read``/``close`` are performed
    together under ``_live_lock``, so a connection cannot be opened in the
    window between deciding "nothing is live" and closing this descriptor.

    Set ``force=True`` only for genuinely offline files (quarantined copies,
    snapshot artifacts, archives) that no live connection can reference.
    zqrefusing byte-level read of %s: a live connection exists in this process and close() would cancel its POSIX locksNrb)r,   r   r   r`   ra   openreadr   )r   ro   rp   handles       r   read_header_bytes_preopenrw   [  s   , 
   	d'888LLH  
        	dD!! +V{{6**+ + + + + + +       + + + + + + + + + 	 	 	       	+                 sY   5B?BB	0B	B	BB	BB?
B.B?-B..B??CCc                      e Zd ZdZdS )LiveConnectionErrorzGA raw file operation was attempted on a database with live connections.Nr
   r   r   r   ry   ry     s        QQQQr   ry   ru   )whatrz   c             #     K   t           5  t          |           t          v rt          d| d|  d          dV  ddd           dS # 1 swxY w Y   dS )u  Hold the connection-lifecycle lock across a raw read of a database file.

    Checking :func:`has_live_connection` and *then* doing the raw I/O is a
    check/use race: a connection can be opened in the window between the two,
    and the raw ``close()`` will cancel its POSIX advisory locks — the exact
    failure class the registry exists to prevent. Any multi-step raw access
    (copying a database plus its ``-wal``/``-shm``/``-journal`` sidecars,
    hashing a file, moving a bundle aside) must therefore run *inside* this
    context manager rather than after a bare check.

    While held, :func:`connect_tracked` blocks, so no new connection can
    appear mid-copy. Raises :class:`LiveConnectionError` if a connection is
    already live when the guard is entered.

    The lock is only held for the duration of the raw I/O; it never spans
    caller work on an open connection, so it does not serialise database use.
    zRefusing to  z: a connection to it is still open in this process, and raw file access would cancel that connection's POSIX advisory locks. Close all database handles (stop the gateway/dashboard) and retry.N)r,   r   r   ry   )r   rz   s     r   offline_file_accessr}     s      & 
  ::***%Bt B Bd B B B   	                 s   1AAA)r   r   r   r   )r   r   r   r   )r   r   r   r)   )r   r   r   r4   )rE   rF   r   rF   )r   r   rM   rO   r   r   )r   r   rW   r   r   r   )r   r   r   r]   )r   r   r   rg   )r   r   ro   rb   rp   r4   r   rq   )r   r   rz   r   ))r   
__future__r   
contextlibloggingri   r#   	threadingpathlibr   typingr   	getLoggerr   r`   SQLITE_HEADER_MAGIC_HEADER_PAGE_COUNT_OFFSETRLockr,   r   r@   RuntimeErrorr	   r   r(   r/   r3   r6   r8   rI   rC   rD   rL   rX   rS   rf   rm   rw   ry   contextmanagerr}   r   r   r   <module>r      s  ; ; ;z # " " " " "      				                 		8	$	$,    Y_
$&  & & & &             &C C C C- - - -/ / / /) ) ) ) ) ) ) )$    (:   ( ,.  - - - -   . (,	? ? ? ? ? ?D   *   *   @ 	" " " " " "JR R R R R, R R R 9?        r   