mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-27 00:29:13 +00:00
* fix(update): clear a stale .git/index.lock before Update Code A git that subprocess.run's timeout kills (SIGKILL) cannot remove .git/index.lock, so every later update failed with "Unable to create .../.git/index.lock: File exists" until the file was removed by hand. Update Code and the automatic update's rollback now remove the lock when no git process is running in the checkout and the file is more than a few seconds old. While another git is running, Update Code says so instead of failing; when the lock could not be checked, the error names the rm command that fixes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvFtgthKUNEPhJu6RuX9Sk * fix(update): count a git that names the checkout in its arguments as running git --git-dir/--work-tree can work on the checkout from any working directory, so the cwd check alone could take such a git's live lock for a stale one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvFtgthKUNEPhJu6RuX9Sk * fix(update): resolve a git's relative path arguments against its cwd `git -C LEDMatrix ...` or a relative --git-dir started outside the checkout works on it too; resolve each argument against the process's working directory before deciding a lock is stale, and count unreadable arguments as naming the checkout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvFtgthKUNEPhJu6RuX9Sk --------- Co-authored-by: Claude <noreply@anthropic.com>
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""Clear a stale ``.git/index.lock`` before Update Code touches the checkout.
|
|
|
|
git creates ``.git/index.lock`` while it rewrites the index and removes it when
|
|
it finishes, or when it is stopped with SIGTERM/SIGINT. A git that dies any
|
|
other way leaves the file behind: ``subprocess.run(..., timeout=...)`` SIGKILLs
|
|
its child when the timeout expires (a slow pull or checkout on a Pi), and a
|
|
power cut or an OOM kill does the same. From then on every git command that
|
|
writes the index fails with::
|
|
|
|
fatal: Unable to create '.../.git/index.lock': File exists.
|
|
|
|
and Update Code can never succeed again until someone removes it over SSH.
|
|
|
|
``clear_stale_index_lock`` removes the file only when it is provably stale: no
|
|
git process is running in this checkout, and the file is old enough that no
|
|
git can have just created it. When it cannot tell (no ``/proc``), it leaves
|
|
the file alone. Standard library only.
|
|
"""
|
|
import logging
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#: A lock younger than this is left alone even when no git process is seen,
|
|
#: so a git that started between the process scan and the removal keeps it.
|
|
MIN_STALE_AGE_SECONDS = 5
|
|
|
|
#: ``clear_stale_index_lock`` outcomes.
|
|
ABSENT = 'absent' # no lock file: nothing to do
|
|
REMOVED = 'removed' # it was stale and has been removed
|
|
IN_USE = 'in_use' # a git process is running here (or the lock is brand new)
|
|
UNKNOWN = 'unknown' # could not tell whether git is running; left alone
|
|
|
|
_PROC = Path('/proc')
|
|
|
|
|
|
def index_lock_path(project_dir):
|
|
"""``<project_dir>/.git/index.lock``, or None when ``.git`` is not a directory."""
|
|
git_dir = Path(project_dir) / '.git'
|
|
return git_dir / 'index.lock' if git_dir.is_dir() else None
|
|
|
|
|
|
def _is_within(path, root):
|
|
try:
|
|
Path(path).relative_to(root)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def git_running_in(project_dir, proc_root=_PROC):
|
|
"""True when a git process may be working in ``project_dir``.
|
|
|
|
Scans ``/proc`` for processes named ``git`` or ``git-*`` (``git-remote-https``
|
|
and friends) whose working directory is inside the checkout, or whose
|
|
arguments name a path in it (``git -C ...``, ``--git-dir=...``). A git process
|
|
whose working directory cannot be read (another user's) counts as running
|
|
here: it might be. Returns None when ``/proc`` is unavailable.
|
|
"""
|
|
proc_root = Path(proc_root)
|
|
if not proc_root.is_dir():
|
|
return None
|
|
root = Path(project_dir).resolve()
|
|
try:
|
|
entries = list(proc_root.iterdir())
|
|
except OSError:
|
|
return None
|
|
for entry in entries:
|
|
if not entry.name.isdigit():
|
|
continue
|
|
try:
|
|
comm = (entry / 'comm').read_text().strip()
|
|
except OSError:
|
|
continue # exited meanwhile
|
|
if comm != 'git' and not comm.startswith('git-'):
|
|
continue
|
|
try:
|
|
cwd = os.readlink(entry / 'cwd')
|
|
except FileNotFoundError:
|
|
continue # exited meanwhile
|
|
except OSError:
|
|
return True
|
|
if _is_within(cwd, root) or _names_checkout(entry, cwd, root):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _names_checkout(entry, cwd, root):
|
|
"""True when the process's arguments name a path in the checkout
|
|
(``-C``, ``--git-dir``, ``--work-tree``, absolute or relative to its
|
|
working directory): git can work on it from anywhere. Unreadable
|
|
arguments count as naming it."""
|
|
try:
|
|
args = os.fsdecode((entry / 'cmdline').read_bytes()).split('\0')
|
|
except FileNotFoundError:
|
|
return False # exited meanwhile
|
|
except OSError:
|
|
return True
|
|
for arg in args[1:]:
|
|
value = arg.split('=', 1)[1] if arg.startswith('--') and '=' in arg else arg
|
|
if value and _is_within(os.path.normpath(os.path.join(cwd, value)), root):
|
|
return True
|
|
return False
|
|
|
|
|
|
def clear_stale_index_lock(project_dir, proc_root=_PROC, now=None):
|
|
"""Remove ``.git/index.lock`` when no git process can still own it.
|
|
|
|
Returns one of ABSENT, REMOVED, IN_USE or UNKNOWN.
|
|
"""
|
|
lock = index_lock_path(project_dir)
|
|
if lock is None:
|
|
return ABSENT
|
|
try:
|
|
mtime = lock.stat().st_mtime
|
|
except FileNotFoundError:
|
|
return ABSENT
|
|
except OSError:
|
|
return UNKNOWN
|
|
running = git_running_in(project_dir, proc_root)
|
|
if running is None:
|
|
return UNKNOWN
|
|
age = (time.time() if now is None else now) - mtime
|
|
if running or age < MIN_STALE_AGE_SECONDS:
|
|
return IN_USE
|
|
try:
|
|
lock.unlink()
|
|
except FileNotFoundError:
|
|
return ABSENT # its owner finished after all
|
|
except OSError as exc:
|
|
logger.warning("Could not remove stale git lock %s: %s", lock, exc)
|
|
return UNKNOWN
|
|
logger.warning("Removed stale git lock %s (%.0fs old, no git process running); "
|
|
"a previous git command was killed before it finished", lock, age)
|
|
return REMOVED
|
|
|
|
|
|
def manual_fix_hint(project_dir):
|
|
"""The sentence shown when a lock blocks the update and was not removed."""
|
|
return (f"If no other git command is running, remove the stale lock with "
|
|
f"rm -f {Path(project_dir) / '.git' / 'index.lock'} and try again.")
|