
    6jS                        U d Z ddlZddlZddlZddlmZ ddlmZmZm	Z	 	 ddlm
Z
 n# e$ r dZ
Y nw xY wddddZ ej                    Zda ej                    ZdZd	efd
Zdeded	efdZddZd	efdZd	efdZ e            Zeed<    e            Zeed<   d Zd Zd Zd Z d dZ!d!de"d	dfdZ#ded	efdZ$d Z%d Z&dS )"aJ  
Hermes Web UI -- Self-update checker.

Checks if the webui and hermes-agent git repos are behind their upstream
branches. Results are cached server-side (30-min TTL) so git fetch runs
at most twice per hour regardless of client count.

Skips repos that are not git checkouts (e.g. Docker baked images where
.git does not exist).
    N)Path)	REPO_ROOTSTREAMSSTREAMS_LOCK)
_AGENT_DIR)webuiagent
checked_atFi  returnc                  l    t           5  t          t                    cddd           S # 1 swxY w Y   dS )ao  Return the current in-memory chat stream count.

    Self-update schedules an in-process re-exec after git pull/reset.  That is
    restart-equivalent for live streams, even when systemd does not see a unit
    restart.  Refuse update/force-update while a stream exists so a browser
    update click cannot recreate the pending-message loss class fixed in #1543.
    N)r   lenr        !/root/hermes-webui/api/updates.py_active_stream_countr      sw     
  7||                 s   )--targetactive_streamsc                 :    |dk    rdnd}dd|  d| d| d| d	|d
S )N   s FzCannot update z while z active chat streamzD is running. Wait for the response to finish, then retry the update.T)okmessager   restart_blockedr   r   )r   r   plurals      r   _restart_blocked_responser   +   sh    "a''SSRFRV R RN R Rv R R R (	 	 	r   
   c                 (   	 t          j        dg| z   t          |          dd|          }|j                                        }|j                                        }|j        dk    r|dfS |p|p	d|j         dfS # t           j        $ r`}t          |dd          pt          |d	d          pd
                                }|pdd	                    |            d| ddfcY d}~S d}~wt          $ r Y dS t          $ r}d| dfcY d}~S d}~ww xY w)zRun a git command and return (useful output, ok).

    On failure, returns stderr (or stdout as fallback) so callers can
    surface actionable git error messages instead of empty strings.
    gitT)cwdcapture_outputtexttimeoutr   zgit exited with status FstderrNstdoutr   zgit  z timed out after r   )zgit executable not foundFzgit failed to start: )
subprocessrunstrr%   stripr$   
returncodeTimeoutExpiredgetattrjoinFileNotFoundErrorOSError)argsr    r#   rr%   r$   excdetails           r   _run_gitr5   9   sy   4NGdNCw
 
 
 !!!!<14<KK#KQ\#K#KURR$ S S S#x..T'#x2N2NTRT[[]]KKKKKKKURRRRRRR 1 1 1000 4 4 4,s,,e33333334s=   A*A= -A= =DAC'!D'D7	D DDDc                  :   t          g dt          d          \  } }|r| r| S t          dz  dz  }|                                rW	 ddl}|                    d|                    d	
                    }|r|                    d          S n# t          $ r Y nw xY wdS )u  Detect the running WebUI version from git or a baked-in fallback file.

    Resolution order:
      1. ``git describe --tags --always --dirty`` — works in any git checkout.
         Returns the exact tag on tagged commits (e.g. ``v0.50.124``), a
         post-tag descriptor between releases (e.g. ``v0.50.124-1-ge91325d``),
         or a bare SHA when no tags exist (shallow clones, fresh forks).
      2. ``api/_version.py`` — a fallback written by the Docker / CI release
         workflow when ``.git`` is not present in the image.  Expected to define
         ``__version__ = 'vX.Y.Z'``.
      3. ``'unknown'`` — last resort; displayed as-is in the settings badge.
    describez--tagsz--alwaysz--dirty   r#   apiz_version.pyr   Nz"__version__\s*=\s*['"]([^'"]+)['"]utf-8encodingr   unknown)r5   r   existsresearch	read_textgroup	Exception)outr   version_file_rems        r   _detect_webui_versionrJ   R   s     DDDiYZ[[[GC	 c 

 u$}4L 
		

9&&&88 A  "wwqzz!" 	 	 	D	 9s   AB 
BBc                  ~   t           dS t          t                     dz  } 	 |                                 r,|                     d                                          }|r|S n# t
          $ r Y nw xY wt          t                                                     sdS t          g dt           d          \  }}|r|r|S dS )	z7Detect the running Hermes Agent version for UI display.Nznot detectedVERSIONr<   r=   r7   r9   r:   )r   r   r@   rC   r*   rE   r5   )rG   r"   rF   r   s       r   _detect_agent_versionrM   x   s    ~
##i/L   	))7);;AACCD    
 
""$$ ~ DDDjZ[\\\GC	 c 
>s   ?A# #
A0/A0WEBUI_VERSIONAGENT_VERSIONc                 F   | s| S |                                  } |                     d          r,|                     ddd                              ddd          } |                     d          } |                     d          r
| dd         } |                     d          S )	uO  Return the browser-facing repository URL for update compare links.

    Git remotes may be HTTPS or SSH and may include a literal ``.git`` suffix.
    Strip only that literal suffix — never use ``str.rstrip('.git')`` because it
    treats the argument as a character set and can truncate ``hermes-webui`` to
    ``hermes-webu``.
    zgit@:/r   zhttps://.gitN)r*   
startswithreplacerstripendswith)
remote_urls    r   _normalize_remote_urlrZ      s      !!##JV$$ T''S!44<<VZQRSS
""3''J6"" %_
S!!!r   c                 L    d| vrd| fS |                      dd          \  }}||fS )zrSplit 'origin/branch-name' into ('origin', 'branch-name').

    Returns (None, ref) if ref contains no slash.
    rR   Nr   )split)refremotebranchs      r   _split_remote_refr`      s8    
 #~~SyYYsA&&NFF6>r   c                     t          ddg|           \  }}|r|r|                    d          d         S dD ]!}t          ddd| g|           \  }}|r|c S "d	S )
z2Detect the remote default branch (master or main).zsymbolic-refzrefs/remotes/origin/HEADrR   )mastermain	rev-parsez--verifyorigin/rc   )r5   r\   )pathrF   r   r_   _s        r   _detect_default_branchri      s    (BCTJJGC	 "c "yy~~b!!$  +z3EV3E3EFMM2 	MMM	8r   c                 R   | | dz                                   sdS t          g d| d          \  }}|s|dddS t          g d	|           \  }}|r|r|}nt          |           }d
| }t          ddd| g|           \  }}|r#|                                rt	          |          nd}	t          dd|g|           \  }
}|r!|
rt          dd|
g|           \  }}|r|r|nd}nd}t          dd|g|           \  }}t          g d|           \  }}t          |          }||	||||dS )zACheck if a git repo is behind its upstream. Returns dict or None.NrS   fetchoriginz--quiet   r:   r   zfetch failed)namebehinderrorre   z--abbrev-refz@{upstream}rf   zrev-listz--countzHEAD..z
merge-baseHEADre   z--short)r^   zget-urlrm   )ro   rp   current_sha
latest_shar_   repo_url)r@   r5   ri   isdigitintrZ   )rg   ro   rh   fetch_okupstreamr   compare_refr_   rF   rp   mb_fullmb_okshortcurrentlatestrY   s                   r   _check_repor      s   |D6M1133|t 9994LLLKAx DNCCC HHH$OOLHb	 )h )'--((( 
I/E/E/EFMMGC44SXXX1F2 |V[A4HHNGU  k9g>EE	r353%%t+y+>EEIFA <<<dCCMJ&z22J   r   c                 `   t           5  | sJt          j                    t          d         z
  t          k     r t	          t                    cddd           S t
          r t	          t                    cddd           S daddd           n# 1 swxY w Y   	 t          t          d          }t          t          d          }t           5  |t          d<   |t          d<   t          j                    t          d<   t	          t                    cddd           daS # 1 swxY w Y   	 dadS # daw xY w)z6Return cached update status for webui and agent repos.r
   NTr   r	   F)	_cache_locktime_update_cache	CACHE_TTLdict_check_in_progressr   r   r   )force
webui_info
agent_infos      r   check_for_updatesr     s    
 " " 	'}\'BBYNN&&" " " " " " " "  	'&&	" " " " " " " "
 "" " " " " " " " " " " " " " "# G44
 W55
 	' 	'%/M'"%/M'"*.)++M,'&&		' 	' 	' 	' 	' 	' 	' #	' 	' 	' 	' 	' 	' 	' 	' 	' #U""""sO   A B
B
<B

BB1D) AD
D) DD)  D!D) )D-       @delayc                 z     ddl ddl fd}t          j        |d                                           dS )u  Re-exec this process after *delay* seconds.

    Called after a successful update so that the freshly-pulled code is
    loaded on the next request, rather than running with a mix of old and
    new Python modules in sys.modules.

    os.execv() replaces the current process image with a fresh interpreter
    running the same argv — sessions are preserved on disk, the HTTP port
    is reclaimed within the delay window, and the client's own
    ``setTimeout(() => location.reload(), 2500)`` lands after the restart.

    Coordinates with ``_apply_lock``: when the user updates both webui
    and agent, the client POSTs them sequentially.  Without coordination
    the restart timer scheduled by the first update's success would fire
    while the second update's git-pull is still running, killing it mid-
    stream and leaving the second repo in an unknown partial state.
    Blocking on ``_apply_lock`` before ``os.execv`` means a pending
    second update always completes before the restart happens.
    r   Nc                     dd l }  | j                   t          5  	                     j        j        gj        z              n%# t          $ r                     d           Y nw xY wd d d            d S # 1 swxY w Y   d S )Nr   )r   sleep_apply_lockexecv
executableargvrE   _exit)r   r   ossyss    r   _doz_schedule_restart.<locals>._do;  s    
5  	 	#.)9CH)DEEEE    	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s4   A;)A	A;	A+(A;*A++A;;A?A?T)r   daemon)r   r   	threadingThreadstart)r   r   r   r   s   ` @@r   _schedule_restartr   $  sf    ( IIIJJJ      & C---3355555r   c                    t                      }|rt          | |          S t                              d          sdddS 	 | dk    rt          }n/| dk    rt
          }n!dd|  dt                                           S ||d	z                                  sdd
dt                                           S t          g d|d          \  }}|sdddt                                           S t          g d|          \  }}|r|r|}nt          |          }d| }t          ddg|           t          dd|g|          \  }}|s"dd| ddt                                           S t          5  dt          d<   ddd           n# 1 swxY w Y   t                       d|  d| | ddt                                           S # t                                           w xY w)u  Force-reset the target repo to the latest remote HEAD.

    Unlike apply_update() which requires a clean working tree and refuses
    merge conflicts, this discards all local modifications (checkout .) and
    resets to origin/<branch> — equivalent to what the diverged/conflict
    error messages ask the user to run manually.

    Should only be called when apply_update() has already returned a
    response with ``conflict: True`` or ``diverged: True`` and the user
    has confirmed they want to discard local changes.
    FblockingUpdate already in progressr   r   r   r	   Unknown target: NrS   Not a git repositoryrk   rn   r:   z=Could not reach the remote repository. Check your connection.rr   rf   checkout.resetz--hardzForce reset to z failedr   r
   Tz force-updated to r   r   r   restart_scheduled)r   r   r   acquirer   r   releaser@   r5   ri   r   r   r   )	r   r   rg   rh   ry   rz   r   r{   r_   s	            r   apply_force_updater   Q  s    *++N A(@@@.. F(DEEE+WDDwDD,Gv,G,GHHJ 	G <v5577<,BCCD 	A ===tRPPP8 	Z < 	3   L L LdSS" 	-( 	-"KK+D11F,F,,K 	*c"D)))'8[94@@2 	T,Rk,R,R,RSS 	  	, 	,*+M,'	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	  AAKAA!%	
 
 	sI   #G ?G 6G -A"G )G 0F;G FG FG G c                    t                      }|rt          | |          S t                              d          sdddS 	 t	          |           t                                           S # t                                           w xY w)z5Stash, pull --ff-only, pop for the given target repo.Fr   r   r   )r   r   r   r   _apply_update_innerr   )r   r   s     r   apply_updater     s    )++N A(@@@.. F(DEEE"6**s   A* *Bc                 h   | dk    rt           }n| dk    rt          }ndd|  dS ||dz                                  sdddS t          g d	|          \  }}|r|r|}nt	          |          }d
| }t          g d|d          \  }}|sdddS t          g d|          \  }}	|	sdd|dd          dS t          d |                                D                       r0dd|  dt          |          z   dz   t          |          z   dz   ddS d}
|rt          dg|          \  }}|sdddS d}
t          |          \  }}ddg}|r|	                    ||g           n|
                    |           t          ||d          \  }}|s|
rt          ddg|           |                                }d|v sd |v r3dd|  d!t          |          z   d"z   t          |          z   d#z   |z   dd$S d%|v sd&|v rdd|  d't          |          z   d(z   |z   dS |                                r|                                dd)         nd*}dd+| dS |
rt          ddg|          \  }}|sdd,dd-S t          5  d.t          d/<   ddd           n# 1 swxY w Y   t                       d|  d0| dd1S )2z?Inner implementation of apply_update, called under _apply_lock.r   r	   Fr   r   NrS   r   rr   rf   rk   rn   r:   zTCould not reach the remote repository. Check your internet connection and try again.)statusz--porcelainz--untracked-files=nozFailed to inspect repo status:    c              3   .   K   | ]}|d d         dv V  d S )N   >   AAAUDDDUUAUDUUr   ).0lines     r   	<genexpr>z&_apply_update_inner.<locals>.<genexpr>  sD       0 0 8AA 0 0 0 0 0 0r   z
The local zX repo has unresolved merge conflicts. To reset to the latest remote version run: git -C z checkout . && git -C z pull --ff-onlyT)r   r   conflictstashzFailed to stash local changespullz	--ff-only   popznot possible to fast-forwarddivergedzk repo has commits that are not on the remote branch, so a fast-forward update is not possible. Run: git -C z fetch origin && git -C z reset --hard )r   r   r   zdoes not trackzno tracking informationz@ branch has no upstream tracking branch configured. Run: git -C z branch --set-upstream-to=i,  z(no output from git)zPull failed: z3Updated but stash pop failed -- manual merge needed)r   r   stash_conflictr   r
   z updated successfullyr   )r   r   r@   r5   ri   any
splitlinesr)   r`   extendappendlowerr*   r   r   r   )r   rg   rz   r   r{   r_   rh   ry   
status_out	status_okstashedr^   	pull_argspull_outpull_ok
pull_lowerr4   pop_oks                     r   r   r     s   	7		(C6(C(CDDD|D6M1133|(>??? HHH$OOLHb	 )h )'--((( 9994LLLKAx 
@
 
 	
 %9994 J	  ^(\*UYVYUYJZ(\(\]]]
 0 0!,,..0 0 0 0 0 
 V   II&)  II& )::
 	
 	
 		
 G 'D))2 	M,KLLL
 '{33NFF%I &&&)****%%% D"===Hg B 	-gu%t,,, ^^%%
)Z77:;S;S# # # #%(YY/2 !$D		* -== @KK
 !	 	 	 z))-F*-T-T# # # #%(YY/1MNP[\   ,4>>+;+;W!!$3$''AW(@(@(@AAA  gu-t44	6 	P"&   
 ( (&'l#( ( ( ( ( ( ( ( ( ( ( ( ( ( (  333!	  s   :JJJ)r   )F)r   )'__doc__r'   r   r   pathlibr   
api.configr   r   r   r   ImportErrorr   Lockr   r   r   r   rx   r   r)   r   r   r5   rJ   rM   rN   __annotations__rO   rZ   r`   ri   r   r   floatr   r   r   r   r   r   r   <module>r      sn  	 	 	                7 7 7 7 7 7 7 7 7 7%%%%%%%   JJJ Q??in in		c 	 	 	 	c 3 4    4 4 4 42#s # # # #Ls    : +*,,s , , ,**,,s , , ," " "&    D D DN# # # #0*6 *6U *6T *6 *6 *6 *6Z=s =t = = = =@  B B B B Bs   ( 22