awful.spawn: Add an `once` and `single_instance` methods.
This commit adds a way to leverage the xproperty and startup_id APIs to persist an execution token across restarts. It allows to use `awful.rules` on clients that were executed by a previous Awesome instance. The main limitations of these methods is the lack of entropy used to build the token. If the command is the same in multiple `awful.spawn.once`, then it will not work as expected. To mitigate this issue, the system try to concatenate the `awful.rules` table after the command and hash the resulting string. Given rules are a table, it can have loops and/or issues with keys ordering. The hash function sort and limite recursion to prevent a stack overflow. Another issue is the unreliability of startup notifications.
This commit is contained in:
parent
9250610a77
commit
927e7b2796
|
@ -8,7 +8,7 @@
|
|||
|
||||
-- Grab environment we need
|
||||
local gdebug = require("gears.debug")
|
||||
local spawn = require("awful.spawn")
|
||||
local spawn = nil --TODO v5 deprecate
|
||||
local set_shape = require("awful.client.shape").update.all
|
||||
local object = require("gears.object")
|
||||
local grect = require("gears.geometry").rectangle
|
||||
|
@ -1138,8 +1138,10 @@ end
|
|||
-- @tparam bool|function merge If true then merge tags (select the client's
|
||||
-- first tag additionally) when the client is not visible.
|
||||
-- If it is a function, it will be called with the client as argument.
|
||||
-- @see awful.spawn.once
|
||||
-- @see awful.spawn.single_instance
|
||||
--
|
||||
-- @function awful.client.run_or_raise
|
||||
-- @deprecated awful.client.run_or_raise
|
||||
-- @usage -- run or raise urxvt (perhaps, with tabs) on modkey + semicolon
|
||||
-- awful.key({ modkey, }, 'semicolon', function ()
|
||||
-- local matcher = function (c)
|
||||
|
@ -1148,6 +1150,10 @@ end
|
|||
-- awful.client.run_or_raise('urxvt', matcher)
|
||||
-- end);
|
||||
function client.run_or_raise(cmd, matcher, merge)
|
||||
gdebug.deprecate("Use awful.spawn.single_instance instead of"..
|
||||
"awful.client.run_or_raise", {deprecated_in=5})
|
||||
|
||||
spawn = spawn or require("awful.spawn")
|
||||
local clients = capi.client.get()
|
||||
local findex = gtable.hasitem(clients, capi.client.focus) or 1
|
||||
local start = gmath.cycle(#clients, findex + 1)
|
||||
|
|
|
@ -374,6 +374,46 @@ end
|
|||
|
||||
rules.add_rule_source("awful.spawn", apply_spawn_rules, {}, {"awful.rules"})
|
||||
|
||||
local function apply_singleton_rules(c, props, callbacks)
|
||||
local persis_id, info = c.single_instance_id, nil
|
||||
|
||||
-- This is a persistent property set by `awful.spawn`
|
||||
if awesome.startup and persis_id then
|
||||
info = aspawn.single_instance_manager.by_uid[persis_id]
|
||||
elseif c.startup_id then
|
||||
info = aspawn.single_instance_manager.by_snid[c.startup_id]
|
||||
aspawn.single_instance_manager.by_snid[c.startup_id] = nil
|
||||
elseif aspawn.single_instance_manager.by_pid[c.pid] then
|
||||
info = aspawn.single_instance_manager.by_pid[c.pid].matcher(c) and
|
||||
aspawn.single_instance_manager.by_pid[c.pid] or nil
|
||||
end
|
||||
|
||||
if info then
|
||||
c.single_instance_id = info.hash
|
||||
gtable.crush(props, info.rules)
|
||||
table.insert(callbacks, info.callback)
|
||||
table.insert(info.instances, c)
|
||||
|
||||
-- Prevent apps with multiple clients from re-using this too often in
|
||||
-- the first 30 seconds before the PID is cleared.
|
||||
aspawn.single_instance_manager.by_pid[c.pid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- The rule source for clients spawned by `awful.spawn.once` and `single_instance`.
|
||||
--
|
||||
-- **Has priority over:**
|
||||
--
|
||||
-- * `awful.rules`
|
||||
--
|
||||
-- **Depends on:**
|
||||
--
|
||||
-- * `awful.spawn`
|
||||
--
|
||||
-- @rulesources awful.spawn_once
|
||||
|
||||
rules.add_rule_source("awful.spawn_once", apply_singleton_rules, {"awful.spawn"}, {"awful.rules"})
|
||||
|
||||
--- Apply awful.rules.rules to a client.
|
||||
-- @client c The client.
|
||||
function rules.apply(c)
|
||||
|
|
|
@ -165,7 +165,9 @@ local lgi = require("lgi")
|
|||
local Gio = lgi.Gio
|
||||
local GLib = lgi.GLib
|
||||
local util = require("awful.util")
|
||||
local timer = require("gears.timer")
|
||||
local gtable = require("gears.table")
|
||||
local gtimer = require("gears.timer")
|
||||
local aclient = require("awful.client")
|
||||
local protected_call = require("gears.protected_call")
|
||||
|
||||
local spawn = {}
|
||||
|
@ -210,6 +212,39 @@ do
|
|||
end
|
||||
end
|
||||
|
||||
local function hash_command(cmd, rules)
|
||||
rules = rules or {}
|
||||
cmd = type(cmd) == "string" and cmd or table.concat(cmd, ';')
|
||||
|
||||
-- Do its best at adding some entropy
|
||||
local concat_rules = nil
|
||||
concat_rules = function (r, depth)
|
||||
if depth > 2 then return end
|
||||
|
||||
local keys = gtable.keys(rules)
|
||||
|
||||
for _, k in ipairs(keys) do
|
||||
local v = r[k]
|
||||
local t = type(v)
|
||||
|
||||
if t == "string" or t == "number" then
|
||||
cmd = cmd..k..v
|
||||
elseif t == "tag" then
|
||||
cmd = cmd..k..v.name
|
||||
elseif t == "table" and not t.connect_signal then
|
||||
cmd = cmd .. k
|
||||
concat_rules(v, depth + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
concat_rules(rules, 1)
|
||||
|
||||
local glibstr = GLib.String(cmd)
|
||||
|
||||
return string.format('%x', math.ceil(GLib.String.hash(glibstr)))
|
||||
end
|
||||
|
||||
spawn.snid_buffer = {}
|
||||
|
||||
function spawn.on_snid_callback(c)
|
||||
|
@ -220,7 +255,7 @@ function spawn.on_snid_callback(c)
|
|||
--TODO v5: Remove this signal
|
||||
c:emit_signal("spawn::completed_with_payload", props, callback)
|
||||
|
||||
timer.delayed_call(function()
|
||||
gtimer.delayed_call(function()
|
||||
spawn.snid_buffer[c.startup_id] = nil
|
||||
end)
|
||||
end
|
||||
|
@ -435,6 +470,160 @@ function spawn.read_lines(input_stream, line_callback, done_callback, close)
|
|||
start_read()
|
||||
end
|
||||
|
||||
-- When a command should only be executed once or only have a single instance,
|
||||
-- track the SNID set on them to prevent multiple execution.
|
||||
spawn.single_instance_manager = {
|
||||
by_snid = {},
|
||||
by_pid = {},
|
||||
by_uid = {},
|
||||
}
|
||||
|
||||
aclient.property.persist("single_instance_id", "string")
|
||||
|
||||
-- Check if the client is running either using the rule source or the matcher.
|
||||
local function is_running(hash, matcher)
|
||||
local status = spawn.single_instance_manager.by_uid[hash]
|
||||
if not status then return false end
|
||||
|
||||
if #status.instances == 0 then return false end
|
||||
|
||||
for _, c in ipairs(status.instances) do
|
||||
if c.valid then return true end
|
||||
end
|
||||
|
||||
if matcher then
|
||||
for _, c in ipairs(client.get()) do
|
||||
if matcher(c) then return true end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Keep the data related to this hash.
|
||||
local function register_common(hash, rules, matcher, callback)
|
||||
local status = spawn.single_instance_manager.by_uid[hash]
|
||||
if status then return status end
|
||||
|
||||
status = {
|
||||
rules = rules,
|
||||
callback = callback,
|
||||
instances = setmetatable({}, {__mode = "v"}),
|
||||
hash = hash,
|
||||
exec = false,
|
||||
matcher = matcher,
|
||||
}
|
||||
|
||||
spawn.single_instance_manager.by_uid[hash] = status
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
local function run_once_common(hash, cmd, keep_pid)
|
||||
local pid, snid = spawn.spawn(cmd)
|
||||
|
||||
if type(pid) == "string" or not snid then return pid, snid end
|
||||
|
||||
assert(spawn.single_instance_manager.by_uid[hash])
|
||||
|
||||
local status = spawn.single_instance_manager.by_uid[hash]
|
||||
status.exec = true
|
||||
|
||||
spawn.single_instance_manager.by_snid[snid] = status
|
||||
|
||||
if keep_pid then
|
||||
spawn.single_instance_manager.by_pid[pid] = status
|
||||
end
|
||||
|
||||
-- Prevent issues related to PID being re-used and a memory leak
|
||||
gtimer {
|
||||
single_shot = true,
|
||||
autostart = true,
|
||||
timeout = 30,
|
||||
callback = function()
|
||||
spawn.single_instance_manager.by_pid [pid ] = nil
|
||||
spawn.single_instance_manager.by_snid[snid] = nil
|
||||
end
|
||||
}
|
||||
|
||||
return pid, snid
|
||||
end
|
||||
|
||||
local function run_after_startup(f)
|
||||
-- The clients are not yet managed during the first execution, so it will
|
||||
-- miss existing instances.
|
||||
if awesome.startup then
|
||||
gtimer.delayed_call(f)
|
||||
else
|
||||
f()
|
||||
end
|
||||
end
|
||||
|
||||
--- Spawn a command if it has not been spawned before.
|
||||
--
|
||||
-- This function tries its best to preserve the state across `awesome.restart()`.
|
||||
--
|
||||
-- By default, when no `unique_id` is specified, this function will generate one by
|
||||
-- hashing the command and its rules. If you have multiple instance of the same
|
||||
-- command and rules, you need to specify an UID or only the first one will be
|
||||
-- executed.
|
||||
--
|
||||
-- The `rules` are standard `awful.rules`.
|
||||
--
|
||||
-- This function depends on the startup notification protocol to be correctly
|
||||
-- implemented by the command. See `client.startup_id` for more information.
|
||||
-- Note that this also wont work with shell or terminal commands.
|
||||
--
|
||||
-- @tparam string|table cmd The command.
|
||||
-- @tparam table rules The properties that need to be applied to the client.
|
||||
-- @tparam[opt] function matcher A matching function to find the instance
|
||||
-- among running clients.
|
||||
-- @tparam[opt] string unique_id A string to identify the client so it isn't executed
|
||||
-- multiple time.
|
||||
-- @tparam[opt] function callback A callback function when the client is created.
|
||||
-- @see awful.rules
|
||||
function spawn.once(cmd, rules, matcher, unique_id, callback)
|
||||
local hash = unique_id or hash_command(cmd, rules)
|
||||
local status = register_common(hash, rules, matcher, callback)
|
||||
run_after_startup(function()
|
||||
if not status.exec and not is_running(hash, matcher) then
|
||||
run_once_common(hash, cmd, matcher ~= nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Spawn a command if an instance is not already running.
|
||||
--
|
||||
-- This is like `awful.spawn.once`, but will spawn new instances if the previous
|
||||
-- has finished.
|
||||
--
|
||||
-- The `rules` are standard `awful.rules`.
|
||||
--
|
||||
-- This function depends on the startup notification protocol to be correctly
|
||||
-- implemented by the command. See `client.startup_id` for more information.
|
||||
-- Note that this also wont work with shell or terminal commands.
|
||||
--
|
||||
-- Note that multiple instances can still be spawned if the command is called
|
||||
-- faster than the client has time to start.
|
||||
--
|
||||
-- @tparam string|table cmd The command.
|
||||
-- @tparam table rules The properties that need to be applied to the client.
|
||||
-- @tparam[opt] function matcher A matching function to find the instance
|
||||
-- among running clients.
|
||||
-- @tparam[opt] string unique_id A string to identify the client so it isn't executed
|
||||
-- multiple time.
|
||||
-- @tparam[opt] function callback A callback function when the client is created.
|
||||
-- @see awful.rules
|
||||
function spawn.single_instance(cmd, rules, matcher, unique_id, callback)
|
||||
local hash = unique_id or hash_command(cmd, rules)
|
||||
register_common(hash, rules, matcher, callback)
|
||||
run_after_startup(function()
|
||||
if not is_running(hash, matcher) then
|
||||
return run_once_common(hash, cmd, matcher ~= nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
capi.awesome.connect_signal("spawn::canceled" , spawn.on_snid_cancel )
|
||||
capi.awesome.connect_signal("spawn::timeout" , spawn.on_snid_cancel )
|
||||
capi.client.connect_signal ("manage" , spawn.on_snid_callback )
|
||||
|
|
Loading…
Reference in New Issue