Skip to content

Use a lock file to avoid exceptions due to concurrenct symlink creation #2851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions tuf/ngclient/_internal/file_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2025, New York University and the TUF contributors
# SPDX-License-Identifier: MIT OR Apache-2.0

"""Platform-specific support for file locking

"""

import os
import sys

Check failure on line 9 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (I001)

tuf/ngclient/_internal/file_lock.py:8:1: I001 Import block is un-sorted or un-formatted


if sys.platform in ['win32']:

Check failure on line 12 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (Q000)

tuf/ngclient/_internal/file_lock.py:12:21: Q000 Single quotes found but double quotes preferred
def create_lockfile(filename: str) -> str:
# Use the name of the file but create lockfile in %TEMP%.
fn = os.path.basename(filename)
lockfile = os.path.join(os.getenv("TEMP"), "tuf" + fn)
#try:

Check failure on line 17 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/_internal/file_lock.py:17:9: ERA001 Found commented-out code
# with open(filename, "w+") as f:
# f.truncate(1)

Check failure on line 19 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/_internal/file_lock.py:19:9: ERA001 Found commented-out code
# f.flush()

Check failure on line 20 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/_internal/file_lock.py:20:9: ERA001 Found commented-out code
#except:

Check failure on line 21 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/_internal/file_lock.py:21:9: ERA001 Found commented-out code
# pass
return lockfile

Check failure on line 23 in tuf/ngclient/_internal/file_lock.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (RET504)

tuf/ngclient/_internal/file_lock.py:23:16: RET504 Unnecessary assignment to `lockfile` before `return` statement
else:
def create_lockfile(filename: str) -> str:
return filename
26 changes: 20 additions & 6 deletions tuf/ngclient/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@
from typing import TYPE_CHECKING, cast
from urllib import parse

from filelock import FileLock

from tuf.api import exceptions
from tuf.api.metadata import Root, Snapshot, TargetFile, Targets, Timestamp
from tuf.ngclient._internal.file_lock import create_lockfile
from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet
from tuf.ngclient.config import EnvelopeType, UpdaterConfig
from tuf.ngclient.urllib3_fetcher import Urllib3Fetcher
Expand Down Expand Up @@ -348,7 +351,11 @@
) as temp_file:
temp_file_name = temp_file.name
temp_file.write(data)
os.replace(temp_file.name, filename)
#print(f"filename: {filename}")

Check failure on line 354 in tuf/ngclient/updater.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/updater.py:354:13: ERA001 Found commented-out code

lck = os.path.join(self._dir, ".root.json.lck")
with FileLock(create_lockfile(lck)):
os.replace(temp_file.name, filename)
except OSError as e:
# remove tempfile if we managed to create one,
# then let the exception happen
Expand All @@ -362,9 +369,13 @@
linkname = os.path.join(self._dir, "root.json")
version = self._trusted_set.root.version
current = os.path.join("root_history", f"{version}.root.json")
with contextlib.suppress(FileNotFoundError):
os.remove(linkname)
os.symlink(current, linkname)

#print(f"lck:{lck}")

Check failure on line 373 in tuf/ngclient/updater.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (ERA001)

tuf/ngclient/updater.py:373:9: ERA001 Found commented-out code
lck = os.path.join(self._dir, ".root.json.lck")
with FileLock(create_lockfile(lck)):
with contextlib.suppress(FileNotFoundError):
os.remove(linkname)
os.symlink(current, linkname)

def _load_root(self) -> None:
"""Load root metadata.
Expand All @@ -375,6 +386,7 @@
If metadata is loaded from remote repository, store it in local cache.
"""

Path(self._dir).mkdir(exist_ok=True, parents=True)
# Update the root role
lower_bound = self._trusted_set.root.version + 1
upper_bound = lower_bound + self.config.max_root_rotations
Expand All @@ -386,8 +398,10 @@
root_path = os.path.join(
self._dir, "root_history", f"{next_version}.root.json"
)
with open(root_path, "rb") as f:
self._trusted_set.update_root(f.read())
lck = os.path.join(self._dir, ".root.json.lck")
with FileLock(create_lockfile(lck)):
with open(root_path, "rb") as f:

Check failure on line 403 in tuf/ngclient/updater.py

View workflow job for this annotation

GitHub Actions / test / Lint Test

Ruff (SIM117)

tuf/ngclient/updater.py:402:21: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
self._trusted_set.update_root(f.read())
continue
except (OSError, exceptions.RepositoryError) as e:
# this root did not exist locally or is invalid
Expand Down
Loading