fix racy sbang (#33549)
Spack currently creates a temporary sbang that is moved "atomically" in place, but this temporary causes races when multiple processes start installing sbang. Let's just stick to an idempotent approach. Notice that we only re-install sbang if Spack updates it (since we do file compare), and sbang was only touched 18 times in the past 6 years, whereas we hit the sbang tempfile issue frequently with parallel install on a fresh spack instance in CI. Also fixes a bug where permissions weren't updated if config changed but the latest version of the sbang file was already installed.
This commit is contained in:
parent
4a5e68816b
commit
4d28a64661
@ -1000,16 +1000,45 @@ def hash_directory(directory, ignore=[]):
|
|||||||
return md5_hash.hexdigest()
|
return md5_hash.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _try_unlink(path):
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
# But if that fails, that's OK.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@system_path_filter
|
@system_path_filter
|
||||||
def write_tmp_and_move(filename):
|
def write_tmp_and_move(path, mode="w"):
|
||||||
"""Write to a temporary file, then move into place."""
|
"""Write to a temporary file in the same directory, then move into place."""
|
||||||
dirname = os.path.dirname(filename)
|
# Rely on NamedTemporaryFile to give a unique file without races
|
||||||
basename = os.path.basename(filename)
|
# in the directory of the target file.
|
||||||
tmp = os.path.join(dirname, ".%s.tmp" % basename)
|
file = tempfile.NamedTemporaryFile(
|
||||||
with open(tmp, "w") as f:
|
prefix="." + os.path.basename(path),
|
||||||
yield f
|
suffix=".tmp",
|
||||||
shutil.move(tmp, filename)
|
dir=os.path.dirname(path),
|
||||||
|
mode=mode,
|
||||||
|
delete=False, # we delete it ourselves
|
||||||
|
)
|
||||||
|
tmp_path = file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield file
|
||||||
|
except BaseException:
|
||||||
|
# On any failure, try to remove the temporary file.
|
||||||
|
_try_unlink(tmp_path)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Always close the file decriptor
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
# Atomically move into existence.
|
||||||
|
try:
|
||||||
|
os.rename(tmp_path, path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
_try_unlink(tmp_path)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
import llnl.util.lang
|
import llnl.util.lang
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import mkdirp, rename
|
from llnl.util.filesystem import mkdirp, write_tmp_and_move
|
||||||
|
|
||||||
import spack.compilers
|
import spack.compilers
|
||||||
import spack.paths
|
import spack.paths
|
||||||
@ -287,10 +287,8 @@ def _write_section(self, section):
|
|||||||
parent = os.path.dirname(self.path)
|
parent = os.path.dirname(self.path)
|
||||||
mkdirp(parent)
|
mkdirp(parent)
|
||||||
|
|
||||||
tmp = os.path.join(parent, ".%s.tmp" % os.path.basename(self.path))
|
with write_tmp_and_move(self.path) as f:
|
||||||
with open(tmp, "w") as f:
|
|
||||||
syaml.dump_config(data_to_write, stream=f, default_flow_style=False)
|
syaml.dump_config(data_to_write, stream=f, default_flow_style=False)
|
||||||
rename(tmp, self.path)
|
|
||||||
|
|
||||||
except (yaml.YAMLError, IOError) as e:
|
except (yaml.YAMLError, IOError) as e:
|
||||||
raise ConfigFileError("Error writing to config file: '%s'" % str(e))
|
raise ConfigFileError("Error writing to config file: '%s'" % str(e))
|
||||||
|
@ -186,44 +186,39 @@ def install_sbang():
|
|||||||
``sbang`` here ensures that users can access the script and that
|
``sbang`` here ensures that users can access the script and that
|
||||||
``sbang`` itself is in a short path.
|
``sbang`` itself is in a short path.
|
||||||
"""
|
"""
|
||||||
# copy in a new version of sbang if it differs from what's in spack
|
|
||||||
sbang_path = sbang_install_path()
|
sbang_path = sbang_install_path()
|
||||||
if os.path.exists(sbang_path) and filecmp.cmp(spack.paths.sbang_script, sbang_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
# make $install_tree/bin
|
all = spack.spec.Spec("all")
|
||||||
|
group_name = spack.package_prefs.get_package_group(all)
|
||||||
|
config_mode = spack.package_prefs.get_package_dir_permissions(all)
|
||||||
|
group_id = grp.getgrnam(group_name).gr_gid if group_name else None
|
||||||
|
|
||||||
|
# First setup the bin dir correctly.
|
||||||
sbang_bin_dir = os.path.dirname(sbang_path)
|
sbang_bin_dir = os.path.dirname(sbang_path)
|
||||||
|
if not os.path.isdir(sbang_bin_dir):
|
||||||
fs.mkdirp(sbang_bin_dir)
|
fs.mkdirp(sbang_bin_dir)
|
||||||
|
|
||||||
# get permissions for bin dir from configuration files
|
# Set group and ownership like we do on package directories
|
||||||
group_name = spack.package_prefs.get_package_group(spack.spec.Spec("all"))
|
if group_id:
|
||||||
config_mode = spack.package_prefs.get_package_dir_permissions(spack.spec.Spec("all"))
|
os.chown(sbang_bin_dir, os.stat(sbang_bin_dir).st_uid, group_id)
|
||||||
|
os.chmod(sbang_bin_dir, config_mode)
|
||||||
if group_name:
|
|
||||||
os.chmod(sbang_bin_dir, config_mode) # Use package directory permissions
|
|
||||||
else:
|
else:
|
||||||
fs.set_install_permissions(sbang_bin_dir)
|
fs.set_install_permissions(sbang_bin_dir)
|
||||||
|
|
||||||
# set group on sbang_bin_dir if not already set (only if set in configuration)
|
# Then check if we need to install sbang itself.
|
||||||
# TODO: after we drop python2 support, use shutil.chown to avoid gid lookups that
|
try:
|
||||||
# can fail for remote groups
|
already_installed = filecmp.cmp(spack.paths.sbang_script, sbang_path)
|
||||||
if group_name and os.stat(sbang_bin_dir).st_gid != grp.getgrnam(group_name).gr_gid:
|
except (IOError, OSError):
|
||||||
os.chown(sbang_bin_dir, os.stat(sbang_bin_dir).st_uid, grp.getgrnam(group_name).gr_gid)
|
already_installed = False
|
||||||
|
|
||||||
# copy over the fresh copy of `sbang`
|
if not already_installed:
|
||||||
sbang_tmp_path = os.path.join(
|
with fs.write_tmp_and_move(sbang_path) as f:
|
||||||
os.path.dirname(sbang_path),
|
shutil.copy(spack.paths.sbang_script, f.name)
|
||||||
".%s.tmp" % os.path.basename(sbang_path),
|
|
||||||
)
|
|
||||||
shutil.copy(spack.paths.sbang_script, sbang_tmp_path)
|
|
||||||
|
|
||||||
# set permissions on `sbang` (including group if set in configuration)
|
# Set permissions on `sbang` (including group if set in configuration)
|
||||||
os.chmod(sbang_tmp_path, config_mode)
|
os.chmod(sbang_path, config_mode)
|
||||||
if group_name:
|
if group_id:
|
||||||
os.chown(sbang_tmp_path, os.stat(sbang_tmp_path).st_uid, grp.getgrnam(group_name).gr_gid)
|
os.chown(sbang_path, os.stat(sbang_path).st_uid, group_id)
|
||||||
|
|
||||||
# Finally, move the new `sbang` into place atomically
|
|
||||||
os.rename(sbang_tmp_path, sbang_path)
|
|
||||||
|
|
||||||
|
|
||||||
def post_install(spec):
|
def post_install(spec):
|
||||||
|
Loading…
Reference in New Issue
Block a user