Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""One interactive MeshTerm per data directory, and a clear answer when you want two.
|
|
|
|
Every copy of MeshTerm on a machine reads and writes the same ``~/.meshterm`` — a
|
|
checkout, a downloaded build, a second instance for a second radio. Nothing about how it
|
|
was installed changes that. Two running at once will not corrupt anything (the database
|
|
is in WAL mode and every file lands by rename) but each holds the JSON stores in memory
|
|
and rewrites them whole, so the second one to save quietly discards the first one's
|
|
changes. A lost contact is much harder to notice than a crash.
|
|
|
|
So the interactive session takes an exclusive lock on the directory it is using, and a
|
|
second one is turned away with the fix in the message: set ``MESHTERM_HOME``.
|
|
|
|
The lock is held by the operating system rather than written down as a process id in a
|
|
file. A pid file has to answer "is that process still alive?", the answer is different on
|
|
every platform, and it is wrong after a reboot recycles the number — so a crash leaves a
|
|
lock nobody can explain and everybody works around. An OS lock is released when the
|
|
process ends, however it ends.
|
|
|
|
One-shot CLI subcommands do not take it. They are brief, mostly read, and blocking
|
|
``meshterm contacts`` because a menu is open elsewhere would be an obstacle rather than a
|
|
guard.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
__all__ = ["InstanceBusy", "LOCK_NAME", "hold_instance_lock"]
|
|
|
|
#: The lock file, kept beside the data it guards. Empty; only the lock on it matters.
|
|
LOCK_NAME = ".lock"
|
|
|
|
|
|
class InstanceBusy(RuntimeError):
|
|
"""Raised when another interactive MeshTerm already holds this data directory."""
|
|
|
|
def __init__(self, config_dir: Path) -> None:
|
|
"""Build the message, which is mostly the way out of the situation."""
|
|
super().__init__(
|
|
f"Another MeshTerm is already using {config_dir}.\n\n"
|
|
"Two of them sharing one directory will quietly overwrite each other's\n"
|
|
"contacts and settings, so this one stopped instead.\n\n"
|
|
"To run a second one — a downloaded build beside your own, or a second\n"
|
|
"radio — give it a directory of its own:\n\n"
|
|
" MESHTERM_HOME=~/meshterm-other meshterm\n"
|
|
)
|
|
self.config_dir = config_dir
|
|
|
|
|
|
def _try_lock(handle) -> bool:
|
|
"""Take an exclusive, non-blocking lock on an open file; ``False`` if someone has it."""
|
|
if sys.platform == "win32":
|
|
import msvcrt
|
|
|
|
try:
|
|
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
import fcntl
|
|
|
|
try:
|
|
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
|
|
@contextmanager
|
|
def hold_instance_lock(config_dir: Path) -> Iterator[None]:
|
|
"""Hold ``config_dir`` for this process, releasing it however the process ends.
|
|
|
|
Args:
|
|
config_dir: The data directory to claim.
|
|
|
|
Raises:
|
|
InstanceBusy: Another interactive MeshTerm holds it.
|
|
"""
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
path = config_dir / LOCK_NAME
|
|
handle = open(path, "a+b") # noqa: SIM115 - closed in the finally below
|
|
try:
|
|
if not _try_lock(handle):
|
|
raise InstanceBusy(config_dir)
|
|
yield
|
|
finally:
|
|
# Closing releases the lock on every platform. The file itself stays: deleting it
|
|
# would race a second process that has already opened it and is about to lock,
|
|
# which is how one directory ends up handed to two of them.
|
|
handle.close()
|