diff --git a/pkgs/clan-cli/clan_cli/flakes/add.py b/pkgs/clan-cli/clan_cli/flakes/add.py index 84abbef91..b119bac4e 100644 --- a/pkgs/clan-cli/clan_cli/flakes/add.py +++ b/pkgs/clan-cli/clan_cli/flakes/add.py @@ -5,19 +5,22 @@ from pathlib import Path from clan_cli.dirs import user_history_file from ..async_cmd import CmdOut, runforcli +from ..locked_open import locked_open async def add_flake(path: Path) -> dict[str, CmdOut]: user_history_file().parent.mkdir(parents=True, exist_ok=True) # append line to history file # TODO: Make this atomic - lines: set[str] = set() - if user_history_file().exists(): - with open(user_history_file()) as f: - lines = set(f.readlines()) - lines.add(str(path)) - with open(user_history_file(), "w") as f: - f.writelines(lines) + lines: set = set() + old_lines = set() + with locked_open(user_history_file(), "w+") as f: + old_lines = set(f.readlines()) + lines = old_lines | {str(path)} + if old_lines != lines: + f.seek(0) + f.writelines(lines) + f.truncate() return {} diff --git a/pkgs/clan-cli/clan_cli/flakes/history.py b/pkgs/clan-cli/clan_cli/flakes/history.py index 2db34a9b6..c1438ac62 100644 --- a/pkgs/clan-cli/clan_cli/flakes/history.py +++ b/pkgs/clan-cli/clan_cli/flakes/history.py @@ -4,12 +4,14 @@ from pathlib import Path from clan_cli.dirs import user_history_file +from ..locked_open import locked_open + def list_history() -> list[Path]: if not user_history_file().exists(): return [] # read path lines from history file - with open(user_history_file()) as f: + with locked_open(user_history_file()) as f: lines = f.readlines() return [Path(line.strip()) for line in lines] diff --git a/pkgs/clan-cli/clan_cli/locked_open.py b/pkgs/clan-cli/clan_cli/locked_open.py new file mode 100644 index 000000000..0c8c81d43 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/locked_open.py @@ -0,0 +1,15 @@ +import fcntl +from collections.abc import Generator +from contextlib import contextmanager +from pathlib import Path + + +@contextmanager +def locked_open(filename: str | Path, mode: str = "r") -> Generator: + """ + This is a context manager that provides an advisory write lock on the file specified by `filename` when entering the context, and releases the lock when leaving the context. The lock is acquired using the `fcntl` module's `LOCK_EX` flag, which applies an exclusive write lock to the file. + """ + with open(filename, mode) as fd: + fcntl.flock(fd, fcntl.LOCK_EX) + yield fd + fcntl.flock(fd, fcntl.LOCK_UN)