Files
Jean-Pierre MartineauandClaude Opus 5.5 ffe31dac36 Docstrings and comments of meshterm/core are in ASD-STE100
The prose of each docstring and comment in the core package now follows
ASD-STE100 Simplified Technical English, with Canadian spelling: short
sentences, simple verb tenses, no dashes or semicolons, and one word for
one concept (docs/development/writing-style.md has the rules and the
glossary). The meaning and each reason stay. The code does not change:
the AST of each file, without its docstrings, is the same as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:29:03 -04:00

98 lines
3.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""One interactive MeshTerm for each data directory, and a clear message when you want two.
Each copy of MeshTerm on a machine reads and writes the same ``~/.meshterm``: a checkout,
a downloaded build, or a second instance for a second companion. The type of installation
does not change that. Two copies that run at the same time do not corrupt data (the
database is in WAL mode, and each file is written through a rename). But each copy keeps
the JSON stores in memory and writes them again in full. Thus the second copy that writes
removes the changes of the first copy, and it does not tell the user. A lost contact is
much more difficult to see than a crash.
Thus the interactive session takes an exclusive lock on the directory that it uses. A
second session stops, and its message gives the solution: set ``MESHTERM_HOME``.
The operating system holds the lock. MeshTerm does not write a process id in a file. A
pid file must answer "is that process still alive?". The answer is different on each
platform, and it is wrong after a reboot gives the same number to a new process. Thus
after a crash, a pid file leaves a lock that nobody can explain and that each user must
bypass. The operating system releases its lock when the process ends, however it ends.
One-shot CLI subcommands do not take the lock. They are short, and they mostly read. If
the lock blocks ``meshterm contacts`` because a menu is open in another place, the lock is
an obstacle, not 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, next to the data that it guards. It is empty: only the lock on it is
#: important.
LOCK_NAME = ".lock"
class InstanceBusy(RuntimeError):
"""Raised when another interactive MeshTerm already holds this data directory."""
def __init__(self, config_dir: Path) -> None:
"""Make the message, which mostly tells the user how to solve the problem."""
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 it is not free)."""
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, and release 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:
# The close releases the lock on all platforms. The file itself stays. If MeshTerm
# deletes it, the delete races a second process that has opened the file already
# and will lock it next. That race gives one directory to two processes.
handle.close()