From 89daf1f1588189f0fedc0b050408d785729a6833 Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Wed, 13 Dec 2023 14:31:02 -0800 Subject: Introduce locking for concurrent access control (#1557) Fixes #1540 --- src/luarocks/cmd.lua | 21 +++++++++++++++++++++ src/luarocks/cmd/build.lua | 2 ++ src/luarocks/cmd/init.lua | 2 ++ src/luarocks/cmd/install.lua | 2 ++ src/luarocks/cmd/make.lua | 2 ++ src/luarocks/cmd/purge.lua | 12 +++--------- src/luarocks/cmd/remove.lua | 2 ++ src/luarocks/core/cfg.lua | 1 + src/luarocks/fetch.lua | 6 +++++- src/luarocks/fs/lua.lua | 12 ++++++++++++ src/luarocks/fs/unix/tools.lua | 39 +++++++++++++++++++++++++++++++++++++++ src/luarocks/fs/win32/tools.lua | 9 +++++++++ 12 files changed, 100 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/luarocks/cmd.lua b/src/luarocks/cmd.lua index ca79c316..2db0c039 100644 --- a/src/luarocks/cmd.lua +++ b/src/luarocks/cmd.lua @@ -26,6 +26,7 @@ cmd.errorcodes = { UNSPECIFIED = 1, PERMISSIONDENIED = 2, CONFIGFILE = 3, + LOCK = 4, CRASH = 99 } @@ -477,6 +478,8 @@ Enabling completion for Fish: "To enable it, see '"..program.." help path'.") parser:flag("--global", "Use the system tree when `local_by_default` is `true`.") parser:flag("--no-project", "Do not use project tree even if running from a project folder.") + parser:flag("--force-lock", "Attempt to overwrite the lock for commands " .. + "that require exclusive access, such as 'install'") parser:flag("--verbose", "Display verbose output of commands executed.") parser:option("--timeout", "Timeout on network operations, in seconds.\n".. "0 means no timeout (wait forever). Default is ".. @@ -672,10 +675,28 @@ function cmd.run_command(description, commands, external_namespace, ...) end local cmd_mod = cmd_modules[args.command] + + local lock + if cmd_mod.needs_lock then + lock, err = fs.lock_access(path.root_dir(cfg.root_dir), args.force_lock) + if not lock then + local try_force = args.force_lock + and (" - failed to force the lock" .. (err and ": " .. err or "")) + or " - use --force-lock to overwrite the lock" + die("command '" .. args.command .. "' " .. + "requires exclusive access to " .. cfg.root_dir .. + try_force, cmd.errorcodes.LOCK) + end + end + local call_ok, ok, err, exitcode = xpcall(function() return cmd_mod.command(args) end, error_handler) + if lock then + fs.unlock_access(lock) + end + if not call_ok then die(ok, cmd.errorcodes.CRASH) elseif not ok then diff --git a/src/luarocks/cmd/build.lua b/src/luarocks/cmd/build.lua index 56f0e757..16c0aff2 100644 --- a/src/luarocks/cmd/build.lua +++ b/src/luarocks/cmd/build.lua @@ -180,4 +180,6 @@ function cmd_build.command(args) return name, version end +cmd_build.needs_lock = true + return cmd_build diff --git a/src/luarocks/cmd/init.lua b/src/luarocks/cmd/init.lua index 8bccb23f..dcd6b759 100644 --- a/src/luarocks/cmd/init.lua +++ b/src/luarocks/cmd/init.lua @@ -173,4 +173,6 @@ function init.command(args) return true end +init.needs_lock = true + return init diff --git a/src/luarocks/cmd/install.lua b/src/luarocks/cmd/install.lua index c2e947b2..b4f15cb8 100644 --- a/src/luarocks/cmd/install.lua +++ b/src/luarocks/cmd/install.lua @@ -255,4 +255,6 @@ function install.command(args) end end +install.needs_lock = true + return install diff --git a/src/luarocks/cmd/make.lua b/src/luarocks/cmd/make.lua index 8b313bb9..8147b5df 100644 --- a/src/luarocks/cmd/make.lua +++ b/src/luarocks/cmd/make.lua @@ -155,4 +155,6 @@ function make.command(args) end end +make.needs_lock = true + return make diff --git a/src/luarocks/cmd/purge.lua b/src/luarocks/cmd/purge.lua index c300e286..09380ef1 100644 --- a/src/luarocks/cmd/purge.lua +++ b/src/luarocks/cmd/purge.lua @@ -39,18 +39,10 @@ end function purge.command(args) local tree = args.tree - if type(tree) ~= "string" then - return nil, "The --tree argument is mandatory. "..util.see_help("purge") - end - - local results = {} - if not fs.is_dir(tree) then - return nil, "Directory not found: "..tree - end - local ok, err = fs.check_command_permissions(args) if not ok then return nil, err, cmd.errorcodes.PERMISSIONDENIED end + local results = {} search.local_manifest_search(results, path.rocks_dir(tree), queries.all()) local sort = function(a,b) return vers.compare_versions(b,a) end @@ -81,4 +73,6 @@ function purge.command(args) return writer.make_manifest(cfg.rocks_dir, "one") end +purge.needs_lock = true + return purge diff --git a/src/luarocks/cmd/remove.lua b/src/luarocks/cmd/remove.lua index 6bb69ad6..affeda25 100644 --- a/src/luarocks/cmd/remove.lua +++ b/src/luarocks/cmd/remove.lua @@ -72,4 +72,6 @@ function cmd_remove.command(args) return true end +cmd_remove.needs_lock = true + return cmd_remove diff --git a/src/luarocks/core/cfg.lua b/src/luarocks/core/cfg.lua index 9c2728cc..81987cc9 100644 --- a/src/luarocks/core/cfg.lua +++ b/src/luarocks/core/cfg.lua @@ -238,6 +238,7 @@ local function make_defaults(lua_version, target_cpu, platforms, home) MKDIR = "mkdir", RMDIR = "rmdir", CP = "cp", + LN = "ln", LS = "ls", RM = "rm", FIND = "find", diff --git a/src/luarocks/fetch.lua b/src/luarocks/fetch.lua index 723af8b5..09f7a40b 100644 --- a/src/luarocks/fetch.lua +++ b/src/luarocks/fetch.lua @@ -33,21 +33,25 @@ function fetch.fetch_caching(url, mirroring) local name = repo_url:gsub("[/:]","_") local cache_dir = dir.path(cfg.local_cache, name) local ok = fs.make_dir(cache_dir) - if not ok then + local lock = ok and fs.lock_access(cache_dir) + if not (ok and lock) then cfg.local_cache = fs.make_temp_dir("local_cache") cache_dir = dir.path(cfg.local_cache, name) ok = fs.make_dir(cache_dir) if not ok then return nil, "Failed creating temporary cache directory "..cache_dir end + lock = fs.lock_access(cache_dir) end local cachefile = dir.path(cache_dir, filename) if cfg.aggressive_cache and (not name:match("^manifest")) and fs.exists(cachefile) then + fs.unlock_access(lock) return cachefile, nil, nil, true end local file, err, errcode, from_cache = fetch.fetch_url(url, cachefile, true, mirroring) + fs.unlock_access(lock) if not file then return nil, err or "Failed downloading "..url, errcode end diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua index 174fdc88..ec497f88 100644 --- a/src/luarocks/fs/lua.lua +++ b/src/luarocks/fs/lua.lua @@ -283,6 +283,18 @@ end if lfs_ok then +function fs_lua.lock_access(dirname, force) + fs.make_dir(dirname) + if force then + os.remove(dir.path(dirname, "lockfile.lfs")) + end + return lfs.lock_dir(dirname) +end + +function fs_lua.unlock_access(lock) + return lock:free() +end + --- Run the given command. -- The command is executed in the current directory in the dir stack. -- @param cmd string: No quoting/escaping is applied to the command. diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua index b7fd4de5..7106ed81 100644 --- a/src/luarocks/fs/unix/tools.lua +++ b/src/luarocks/fs/unix/tools.lua @@ -315,4 +315,43 @@ function tools.is_superuser() return fs.current_user() == "root" end +function tools.lock_access(dirname, force) + fs.make_dir(dirname) + + local tempfile = os.tmpname() + if not tempfile then + return nil, "failed creating temp file for locking" + end + + local fd, fderr = io.open(tempfile, "w") + if not fd then + return nil, "failed opening temp file " .. tempfile .. " for locking: " .. fderr + end + + local ok, werr = fd:write("lock file for " .. dirname) + if not ok then + return nil, "failed writing temp file " .. tempfile .. " for locking: " .. werr + end + + fd:close() + + local lockfile = dir.path(dirname, "lockfile.luarocks") + + local force_flag = force and " -f" or "" + + if fs.execute(vars.LN .. force_flag, tempfile, lockfile) then + return { + tempfile = tempfile, + lockfile = lockfile, + } + else + return nil, "File exists" -- same message as luafilesystem + end +end + +function tools.unlock_access(lock) + os.remove(lock.lockfile) + os.remove(lock.tempfile) +end + return tools diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua index 9bd050c6..be63063b 100644 --- a/src/luarocks/fs/win32/tools.lua +++ b/src/luarocks/fs/win32/tools.lua @@ -316,4 +316,13 @@ function tools.set_time(filename, time) return true -- FIXME end +function tools.lock_access(dirname) + -- NYI + return {} +end + +function tools.unlock_access(lock) + -- NYI +end + return tools -- cgit v1.2.3-55-g6feb