irongit

Configure synchronization of files and directories through a plugin.

cleaned up state management

huncholanehuncholaneauthored
parent 655a7cacommit 92889086acb644daf103e6c15a3684beb73d806bBrowse files

7 files changed, +99 -45

+2-2lua/syncmap/commands.lua
@@ -10,8 +10,8 @@ function M.setup()
1010 { desc = "Clears everything and starts all the inotify servers again" }
1111 )
1212 vim.api.nvim_create_user_command(
13- "SyncmapStop",
14- state.clear,
13+ "SyncmapClean",
14+ state.clean,
1515 { desc = "Clears the active map and all of it's processes" }
1616 )
1717 vim.api.nvim_create_user_command(
+6-0lua/syncmap/log.lua
@@ -37,4 +37,10 @@ function M.error(msg)
3737 M.log(msg, vim.log.levels.ERROR)
3838 end
3939
40+---Prints a message regardless of log level
41+---@param msg any
42+function M.print(msg)
43+ vim.print("[syncmap] " .. msg)
44+end
45+
4046 return M
+7-3lua/syncmap/rsync.lua
@@ -28,17 +28,19 @@ function M.spawn_watcher(args)
2828 })
2929 end
3030
31+ local tag = string.format("syncmap:%s->%s", args.src, args.dst)
32+
3133 local cmd = string.format(
32- "while inotifywait -r -m -e modify,create,delete %q; do rsync %s %q %q; done #syncmap:%s",
34+ "while inotifywait -r -m -e modify,create,delete %q; do rsync %s %q %q; done; : %s",
3335 args.src,
3436 table.concat(args.flags, " "),
3537 args.src,
3638 args.dst,
37- args.src
39+ tag
3840 )
3941 local cmd_str = "sh -c '" .. cmd .. "'"
4042
41- simple_cmd.spawn("sh", {
43+ local _, pid = simple_cmd.spawn("sh", {
4244 args = { "-c", cmd },
4345 path = "sh",
4446 cwd = args.src,
@@ -46,6 +48,8 @@ function M.spawn_watcher(args)
4648 log.info(string.format("%s\n%d completed with %d code and %d status", cmd_str, pid, code, status))
4749 end,
4850 })
51+ log.info(string.format("%s\nPid is %d", cmd_str, pid))
52+ return pid
4953 end
5054
5155 return M
+27-1lua/syncmap/simple_cmd.lua
@@ -2,6 +2,8 @@ local M = {}
22
33 ---@alias StdIO integer|uv.uv_stream_t|nil
44
5+---@alias SpawnCommandCallback fun(code:integer, signal:integer, handle:uv.uv_process_t, pid:integer)
6+
57 ---@class SpawnCommandParams
68 ---@field args string[] The arguments to run on the exectuable
79 ---@field cwd? string @[default=vim.fn.getcwd()] Set the current working directory for the sub-process.
@@ -13,7 +15,7 @@ local M = {}
1315 ---@field verbatim? boolean @[default=false] If true, do not wrap any arguments in quotes, or perform any other escaping, when converting the argument list into a command line string. This option is only meaningful on Windows systems. On Unix it is silently ignored.
1416 ---@field gid? integer @[default=vim.uv.getgid()] Set the child process' group id.
1517 ---The callback to run after the command has returned
16----@field callback? fun(code:integer, signal:integer, handle:uv.uv_process_t, pid:integer)
18+---@field callback? SpawnCommandCallback
1719 ---@field close? boolean @[default=true] Request the handle to be closed (to simplify callbacks)
1820
1921 ---A wrapper for uv.spawn that includes defaults. This makes it easier to spawn commands in the background. Also automatically closes the handle unless close is specified to false.
@@ -55,4 +57,28 @@ function M.spawn(path, p)
5557 return handle, pid
5658 end
5759
60+---Kills a process and all of its descendents
61+---@param pid integer|string
62+function M.kill(pid)
63+ vim.fn.system({ "kill", "-TERM", "-" .. tostring(pid) })
64+end
65+
66+---Checks if a process is running
67+---@param pid string|integer
68+function M.exists(pid)
69+ local result = false
70+ local done = false
71+ M.spawn("ps", {
72+ args = { "-s", tostring(pid) },
73+ callback = function(code, _, _, _)
74+ result = code == 0
75+ done = true
76+ end,
77+ })
78+ vim.wait(100, function()
79+ return done
80+ end, 10)
81+ return result
82+end
83+
5884 return M
+41-22lua/syncmap/state.lua
@@ -1,6 +1,7 @@
11 local rsync = require("syncmap.rsync")
22 local utils = require("syncmap.utils")
33 local log = require("syncmap.log")
4+local simple_cmd = require("syncmap.simple_cmd")
45 local M = {
56 opts = require("syncmap.default"),
67 }
@@ -28,7 +29,9 @@ local function create_state_file()
2829 end
2930 create_state_file()
3031
31----@alias RunningSrcs table<RsyncPath, boolean>
32+---@alias RunningPid string|integer The process id for a SyncmapWatchItem
33+---@alias RunningTag string A key based on src:dst for a SyncmapWatchItem
34+---@alias RunningSrcs table<RsyncPath, RunningPid>
3235
3336 ---A dictionairy keyed on the src for a match located in ~/.local/share/nvim/syncmap/state.json
3437 ---@type RunningSrcs
@@ -50,30 +53,40 @@ load_state()
5053 ---Removes stale states, calls reverse_rsync on new folders, and starts watch on dead pids
5154 function M.sync()
5255 local opts = M.opts
53- ---@type table<RsyncPath, SyncmapConfigMatch>
56+ ---@type table<RunningTag, SyncmapWatchItem>
5457 local lookup = {}
5558 for _, m in ipairs(opts.map) do
56- lookup[m[1]] = m
59+ local tag = vim.fn.expand(m[1]) .. ":" .. vim.fn.expand(m[2])
60+ lookup[tag] = m
5761 end
5862
59- for src in pairs(M.active) do
60- if not lookup[src] then
61- M.active[src] = nil
62- utils.kill(src)
63- elseif utils.search(src) == "" then
64- local m = lookup[src]
63+ -- Iterate active and sync what exists
64+ for tag, pid in pairs(M.active) do
65+ if not lookup[tag] then
66+ log.info(string.format("Removed from the config.\n%s\nRemoving the process %d.", tag, pid))
67+ M.active[tag] = nil
68+ utils.kill(pid)
69+ elseif not simple_cmd.exists(pid) then
70+ log.info(
71+ string.format(
72+ "In active table but the process %d is not running.\n%s\nStarting a new process.",
73+ pid,
74+ tag
75+ )
76+ )
77+ local m = lookup[tag]
6578 local r = utils.row_to_rsync_params(m)
66- log.info("Couldn't find process for " .. m[1] .. " " .. m[2])
67- rsync.spawn_watcher(r)
68- M.active[r.src] = true
79+ M.active[tag] = rsync.spawn_watcher(r)
6980 end
7081 end
7182
83+ ---Iterate config and add new items
7284 for _, m in ipairs(opts.map) do
73- if not M.active[m[1]] then
85+ local tag = vim.fn.expand(m[1]) .. ":" .. vim.fn.expand(m[2])
86+ if not M.active[tag] then
87+ log.info(string.format("%s\nIn the config but not in the active table. Starting a new process.", tag))
7488 local r = utils.row_to_rsync_params(m)
75- rsync.spawn_watcher(r)
76- M.active[r.src] = true
89+ M.active[tag] = rsync.spawn_watcher(r)
7790 end
7891 end
7992 M.save()
@@ -82,13 +95,13 @@ end
8295 function M.save()
8396 local ok, encoded = pcall(vim.json.encode, M.active)
8497 if not ok then
85- utils.log("Failed to encode state", vim.log.levels.ERROR)
98+ log.error("Failed to encode state")
8699 return
87100 end
88101
89102 local fd = io.open(M.state_file, "w")
90103 if not fd then
91- utils.log("Failed to open state file for writing", vim.log.levels.ERROR)
104+ log.error("Failed to open state file for writing")
92105 return
93106 end
94107
@@ -98,15 +111,17 @@ end
98111
99112 ---Kills all the active inotifywatch servers
100113 function M.killall()
101- for src in pairs(M.active) do
102- utils.kill(src)
114+ local s = ""
115+ for tag, pid in pairs(M.active) do
116+ s = string.format("%s\n%d: %s", s, pid, tag)
117+ simple_cmd.kill(pid)
103118 end
119+ log.info(string.format("Killed the following watch items\n%s", s))
104120 M.active = {}
105121 end
106122
107123 ---Clears the saved information by deleting the state file
108-function M.clear()
109- M.killall()
124+function M.clean()
110125 local ok, err = os.remove(M.state_file)
111126 if not ok then
112127 log.error("Failed to delete state file: " .. err)
@@ -123,7 +138,11 @@ end
123138
124139 ---List active
125140 function M.show_active()
126- vim.print(M.active)
141+ local s = ""
142+ for tag, pid in M.active do
143+ s = string.format("%d: %s", pid, tag)
144+ end
145+ log.print("Current active watchers\n" .. s)
127146 end
128147
129148 ---Shows the current options of Syncmap
+3-3lua/syncmap/types.lua
@@ -27,7 +27,7 @@
2727 ---A match to use for syncing using files or folders.
2828 ---
2929 ---Ex: `{"~/.dotfiles/nvim/", "~/.config/nvim/"}` sync everything within `~/.dotfiles/nvim/` into `~/.config/nvim/`
30----@class SyncmapConfigMatch
30+---@class SyncmapWatchItem
3131 ---@field [1] RsyncPath @[required] Path to sync from
3232 ---@field [2] RsyncPath @[required] Path to sync to
3333 ---@field reverse_sync_on_spawn? ReverseSyncOnSpawn @[default=parent.reverse_sync_on_startup]
@@ -36,7 +36,7 @@
3636
3737 ---Configurations for syncmap
3838 ---@class SyncmapOpts
39----@field map? SyncmapConfigMatch[] @[default={}] The folders and files to keep synchronized
39+---@field map? SyncmapWatchItem[] @[default={}] The folders and files to keep synchronized
4040 ---@field reverse_sync_on_startup? ReverseSyncOnSpawn
4141 ---@field rsync? RsyncFlag[] @[default={"-a", "--delete"}] Rsync flags that will be used if a map item doesn't include anything. I.E. Default flags
4242 ---@field log_level? LogLevel @[default="error"] Sets the log level for syncmap
@@ -44,7 +44,7 @@
4444
4545 ---Final config used in the plugin code. Use `SyncmapOpts` for your config
4646 ---@class FinalSyncmapOpts
47----@field map SyncmapConfigMatch[] @[default={}] The folders and files to keep synchronized
47+---@field map SyncmapWatchItem[] @[default={}] The folders and files to keep synchronized
4848 ---@field reverse_sync_on_startup ReverseSyncOnSpawn
4949 ---@field rsync RsyncFlag[] @[default={"-a", "--delete"}] Rsync flags that will be used if a map item doesn't include anything. I.E. Default flags
5050 ---@field log_level LogLevel @[default="error"] Sets the log level for syncmap
+13-14lua/syncmap/utils.lua
@@ -3,7 +3,7 @@ local M = {
33 }
44
55 ---Correctly extracts reverse_sync_on_spawn
6----@param row SyncmapConfigMatch
6+---@param row SyncmapWatchItem
77 function M.extract_reverse(row)
88 if row.reverse_sync_on_spawn == nil then
99 if M.opts.reverse_sync_on_startup == nil then
@@ -49,7 +49,7 @@ function M.string_to_exclude_from(s)
4949 end
5050
5151 ---Gets the exclude from using parent pattern
52----@param m SyncmapConfigMatch
52+---@param m SyncmapWatchItem
5353 function M.extract_exclude_from(m)
5454 if m.exclude_from == nil then
5555 if M.opts.exclude_from ~= nil then
@@ -62,7 +62,7 @@ function M.extract_exclude_from(m)
6262 end
6363
6464 ---Makes sure to extract flags correctly
65----@param m SyncmapConfigMatch
65+---@param m SyncmapWatchItem
6666 ---@return RsyncFlag[]
6767 function M.extract_flags(m)
6868 local flags = m.rsync
@@ -80,7 +80,7 @@ function M.extract_flags(m)
8080 return flags
8181 end
8282
83----@param m SyncmapConfigMatch
83+---@param m SyncmapWatchItem
8484 function M.row_to_rsync_params(m)
8585 local flags = M.extract_flags(m)
8686 ---@type RsyncParams
@@ -92,16 +92,15 @@ function M.row_to_rsync_params(m)
9292 }
9393 end
9494
95----Kills an inotifywait instance based on the src name
96----@param src string
97-function M.kill(src)
98- vim.fn.system({ "pkill", "-f", "^inotifywait.*" .. src })
99-end
100-
101----Searches for an existing inotifywait instance based on the src name
102----@param src string
103-function M.search(src)
104- return vim.fn.system({ "pgrep", "-f", "^inotifywait.*" .. src })
95+---@param str string
96+---@param sep string
97+---@return string[]
98+function M.split(str, sep)
99+ local result = {}
100+ for part in string.gmatch(str, "([^" .. sep .. "]+)") do
101+ table.insert(result, part)
102+ end
103+ return result
105104 end
106105
107106 return M