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>
98 lines
3.9 KiB
Python
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()
|