diff options
| -rw-r--r-- | .gitlab-ci.yml | 7 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | init.lua | 87 | ||||
| -rw-r--r-- | lockfiles.lua | 84 | ||||
| -rwxr-xr-x | lockfiles_test.lua | 12 |
5 files changed, 120 insertions, 74 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 555786b..ce373b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: muhq/lua-dev:0.1 stages: - check + - test check-luacheck: stage: check @@ -14,3 +15,9 @@ check-format: script: - luarocks install --server=https://luarocks.org/dev luaformatter - make check-format + +test: + stage: test + script: + - luarocks install lunatest + - make test @@ -1,6 +1,7 @@ .PHONY: check format check-luacheck check-format LUA_FILES := $(shell find -name "*.lua") +TEST_FILES := $(shell find -name "*_test.lua") check: check-luacheck check-format @@ -12,3 +13,6 @@ check-format: format: lua-format -i $(LUA_FILES) + +test: + set -e; for tf in $(TEST_FILES); do "$$tf"; done @@ -12,51 +12,23 @@ -- You should have received a copy of the GNU General Public License along with -- vis-lockfiles found in the LICENSE file. -- If not, see <https://www.gnu.org/licenses/>. +local source_str = debug.getinfo(1, 'S').source:sub(2) +local script_path = source_str:match('(.*/)') + +local lockfiles = dofile(script_path .. 'lockfiles.lua') + local M = {} M.directory = '${XDG_STATE_HOME:-${HOME}/.local/state}/vis/locks' M.directory_expanded = nil --- Helper to execute a command and capture its output -local function capture_cmd(cmd) - local p = assert(io.popen(cmd, 'r')) - local s = assert(p:read('*a')) - local success, _, status = p:close() - if not success then - local err = cmd .. ' failed with exit code: ' .. status - vis:info('vis-lockfiles: ' .. err) - end - return s -end - local function get_lock_dir() if M.directory_expanded ~= M.directory then - M.directory = capture_cmd('printf %s "' .. M.directory .. '"') + M.directory = lockfiles.capture_cmd('printf %s "' .. M.directory .. '"') M.directory_expanded = M.directory end return M.directory end -local function lock_file_path(path) - -- escape '%' first - local filename = path:gsub('%%', '%%25'):gsub('/', '%%2F') - return get_lock_dir() .. '/' .. filename .. '.lock' -end - --- determine vis' pid -local vis_pid -do - local vis_proc_file = io.open('/proc/self/stat', 'r') - if vis_proc_file then - vis_pid = vis_proc_file:read('*n') - vis_proc_file:close() - - else -- fallback if /proc/self/stat is not available - local out = capture_cmd('sh -c "echo $PPID"') - vis_pid = tonumber(out) - end -end -assert(vis_pid) - -- get a user confirmation -- return true if user selected yes, false otherwise local function confirm(path) @@ -82,49 +54,15 @@ local function confirm(path) end local created_locks = {} -local function create_lock_file(lfile_path) - local lock_file = io.open(lfile_path) - if lock_file then - lock_file:close() - return false - end - - -- create the lock file with our pid - if not io.open(lfile_path, 'w') then - assert(os.execute('mkdir -p "' .. get_lock_dir() .. '"')) - end - lock_file = assert(io.open(lfile_path, 'w')) - lock_file:write(vis_pid) - lock_file:close() - - -- detect a possible race - lock_file = assert(io.open(lfile_path)) - local lock_pid = lock_file:read('*n') - lock_file:close() - local locked = tonumber(lock_pid) == vis_pid - - -- remember we created the lock file - if locked then - created_locks[lfile_path] = true - end - - return locked -end - -local function delete_lock_file(lfile_path) - local success, err = os.remove(lfile_path) - if not success then - vis:info('vis-lockfiles: ' .. err) - end - created_locks[lfile_path] = nil -end vis.events.subscribe(vis.events.FILE_OPEN, function(file) if not file.path then return end - local lfile_path = lock_file_path(file.path) - if not create_lock_file(lfile_path) and not confirm(lfile_path) then + local lfile_path = lockfiles.lock_file_path(get_lock_dir(), file.path) + if lockfiles.create_lock_file(get_lock_dir(), lfile_path) then + created_locks[lfile_path] = true + elseif not confirm(lfile_path) then os.exit(1) end end) @@ -133,9 +71,10 @@ vis.events.subscribe(vis.events.FILE_CLOSE, function(file) if not file.path then return end - local lfile_path = lock_file_path(file.path) + local lfile_path = lockfiles.lock_file_path(get_lock_dir(), file.path) if created_locks[lfile_path] then - delete_lock_file(lfile_path) + lockfiles.delete_lock_file(lfile_path) + created_locks[lfile_path] = nil end end) diff --git a/lockfiles.lua b/lockfiles.lua new file mode 100644 index 0000000..9e120bd --- /dev/null +++ b/lockfiles.lua @@ -0,0 +1,84 @@ +-- Copyright (c) 2024 Florian Fischer. All rights reserved. +-- +-- vis-lockfiles is free software: you can redistribute it and/or modify it under +-- the terms of the GNU General Public License as published by the Free Software +-- Foundation, either version 3 of the License, or (at your option) any later +-- version. +-- +-- vis-lockfiles is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- vis-lockfiles found in the LICENSE file. +-- If not, see <https://www.gnu.org/licenses/>. +-- Helper to execute a command and capture its output +local function capture_cmd(cmd) + local p = assert(io.popen(cmd, 'r')) + local s = assert(p:read('*a')) + local success, _, status = p:close() + if not success then + local err = cmd .. ' failed with exit code: ' .. status + vis:info('vis-lockfiles: ' .. err) + end + return s +end + +local function lock_file_path(lock_dir, path) + -- escape '%' first + local filename = path:gsub('%%', '%%25'):gsub('/', '%%2F') + return lock_dir .. '/' .. filename .. '.lock' +end + +-- determine vis' pid +local vis_pid +do + local vis_proc_file = io.open('/proc/self/stat', 'r') + if vis_proc_file then + vis_pid = vis_proc_file:read('*n') + vis_proc_file:close() + + else -- fallback if /proc/self/stat is not available + local out = capture_cmd('sh -c "echo $PPID"') + vis_pid = tonumber(out) + end +end +assert(vis_pid) + +local function create_lock_file(lock_dir, lfile_path) + local lock_file = io.open(lfile_path) + if lock_file then + lock_file:close() + return false + end + + -- create the lock file with our pid + if not io.open(lfile_path, 'w') then + assert(os.execute('mkdir -p "' .. lock_dir .. '"')) + end + lock_file = assert(io.open(lfile_path, 'w')) + lock_file:write(vis_pid) + lock_file:close() + + -- detect a possible race + lock_file = assert(io.open(lfile_path)) + local lock_pid = lock_file:read('*n') + lock_file:close() + local locked = tonumber(lock_pid) == vis_pid + + return locked +end + +local function delete_lock_file(lfile_path) + local success, err = os.remove(lfile_path) + if not success then + vis:info('vis-lockfiles: ' .. err) + end +end + +return { + capture_cmd = capture_cmd, + lock_file_path = lock_file_path, + delete_lock_file = delete_lock_file, + create_lock_file = create_lock_file, +} diff --git a/lockfiles_test.lua b/lockfiles_test.lua new file mode 100755 index 0000000..a393f60 --- /dev/null +++ b/lockfiles_test.lua @@ -0,0 +1,12 @@ +#!/usr/bin/env lua5.4 + +local lockfiles = dofile('lockfiles.lua') + +local lunatest = require('lunatest') +function test_percentage_escape() -- luacheck: ignore 111 + local lock_file_path_with_percent = lockfiles.lock_file_path('.', 'foo%bar') + local lock_file_path_with_slash = lockfiles.lock_file_path('.', 'foo/bar') + lunatest.assert_not_equal(lock_file_path_with_percent, lock_file_path_with_slash) +end + +lunatest.run() |
