302 lines
11 KiB
Lua
302 lines
11 KiB
Lua
---------------------------------------------------------------------------
|
|
--- Spawning of programs.
|
|
--
|
|
-- This module provides methods to start programs and supports startup
|
|
-- notifications, which allows for callbacks and applying properties to the
|
|
-- program after it has been launched. This requires currently that the
|
|
-- applicaton supports them.
|
|
--
|
|
-- @author Julien Danjou <julien@danjou.info>
|
|
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
|
|
-- @copyright 2008 Julien Danjou
|
|
-- @copyright 2014 Emmanuel Lepage Vallee
|
|
-- @release @AWESOME_VERSION@
|
|
-- @module awful.spawn
|
|
---------------------------------------------------------------------------
|
|
|
|
local capi =
|
|
{
|
|
awesome = awesome,
|
|
mouse = mouse,
|
|
client = client,
|
|
}
|
|
local lgi = require("lgi")
|
|
local Gio = lgi.Gio
|
|
local GLib = lgi.GLib
|
|
local util = require("awful.util")
|
|
local protected_call = require("gears.protected_call")
|
|
|
|
local spawn = {}
|
|
|
|
|
|
local end_of_file
|
|
do
|
|
-- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled.
|
|
local input
|
|
if not pcall(function()
|
|
-- No idea when this API changed, but some versions expect a string,
|
|
-- others a table with some special(?) entries
|
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data(""))
|
|
end) then
|
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({}))
|
|
end
|
|
local line, length = input:read_line()
|
|
if not line then
|
|
-- Fixed in 2016: NULL on the C side is transformed to nil in Lua
|
|
end_of_file = function(arg)
|
|
return not arg
|
|
end
|
|
elseif tostring(line) == "" and #line ~= length then
|
|
-- "Historic" behaviour for end-of-file:
|
|
-- - NULL is turned into an empty string
|
|
-- - The length variable is not initialized
|
|
-- It's highly unlikely that the uninitialized variable has value zero.
|
|
-- Use this hack to detect EOF.
|
|
end_of_file = function(arg1, arg2)
|
|
return #arg1 ~= arg2
|
|
end
|
|
else
|
|
assert(tostring(line) == "", "Cannot determine how to detect EOF")
|
|
-- The above uninitialized variable was fixed and thus length is
|
|
-- always 0 when line is NULL in C. We cannot tell apart an empty line and
|
|
-- EOF in this case.
|
|
require("gears.debug").print_warning("Cannot reliably detect EOF on an "
|
|
.. "GIOInputStream with this LGI version")
|
|
end_of_file = function(arg)
|
|
return tostring(arg) == ""
|
|
end
|
|
end
|
|
end
|
|
|
|
spawn.snid_buffer = {}
|
|
|
|
function spawn.on_snid_callback(c)
|
|
local props = spawn.snid_buffer[c.startup_id]
|
|
if props then
|
|
c:emit_signal("spawn::completed_with_payload", props)
|
|
spawn.snid_buffer[c.startup_id] = nil
|
|
end
|
|
end
|
|
|
|
function spawn.on_snid_cancel(id)
|
|
if spawn.snid_buffer[id] then
|
|
spawn.snid_buffer[id] = nil
|
|
end
|
|
end
|
|
|
|
--- Spawn a program, and optionally apply properties and/or run a callback.
|
|
--
|
|
-- Applying properties or running a callback requires the program/client to
|
|
-- support startup notifications.
|
|
--
|
|
-- See `awful.rules.execute` for more details about the format of `sn_rules`.
|
|
--
|
|
-- @tparam string|table cmd The command.
|
|
-- @tparam[opt=true] table|boolean sn_rules A table of properties to be applied
|
|
-- after startup; `false` to disable startup notifications.
|
|
-- @tparam[opt] function callback A callback function to be run after startup.
|
|
-- @treturn[1] integer The forked PID.
|
|
-- @treturn[1] ?string The startup notification ID, if `sn` is not false, or
|
|
-- a `callback` is provided.
|
|
-- @treturn[2] string Error message.
|
|
function spawn.spawn(cmd, sn_rules, callback)
|
|
if cmd and cmd ~= "" then
|
|
local enable_sn = (sn_rules ~= false or callback)
|
|
enable_sn = not not enable_sn -- Force into a boolean.
|
|
if not sn_rules and callback then
|
|
sn_rules = {callback=callback}
|
|
elseif callback then
|
|
sn_rules.callback = callback
|
|
end
|
|
local pid, snid = capi.awesome.spawn(cmd, enable_sn)
|
|
-- The snid will be nil in case of failure
|
|
if snid and type(sn_rules) == "table" then
|
|
spawn.snid_buffer[snid] = sn_rules
|
|
end
|
|
return pid, snid
|
|
end
|
|
-- For consistency
|
|
return "Error: No command to execute"
|
|
end
|
|
|
|
--- Spawn a program using the shell.
|
|
-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`).
|
|
-- @tparam string cmd The command.
|
|
function spawn.with_shell(cmd)
|
|
if cmd and cmd ~= "" then
|
|
cmd = { util.shell, "-c", cmd }
|
|
return capi.awesome.spawn(cmd, false)
|
|
end
|
|
end
|
|
|
|
--- Spawn a program and asynchronously and capture its output line by line.
|
|
-- @tparam string|table cmd The command.
|
|
-- @tab callbacks Table containing callbacks that should be
|
|
-- invoked on various conditions.
|
|
-- @tparam[opt] function callbacks.stdout Function that is called with each line of
|
|
-- output on stdout, e.g. `stdout(line)`.
|
|
-- @tparam[opt] function callbacks.stderr Function that is called with each line of
|
|
-- output on stderr, e.g. `stderr(line)`.
|
|
-- @tparam[opt] function callbacks.output_done Function to call when no more output
|
|
-- is produced.
|
|
-- @tparam[opt] function callbacks.exit Function to call when the spawned process
|
|
-- exits. This function gets the exit reason and code as its argument. The
|
|
-- reason can be "exit" or "signal". For "exit", the second argument is the exit
|
|
-- code. For "signal", the second argument is the signal causing process
|
|
-- termination.
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
function spawn.with_line_callback(cmd, callbacks)
|
|
local stdout_callback, stderr_callback, done_callback, exit_callback =
|
|
callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit
|
|
local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil
|
|
local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd,
|
|
false, false, have_stdout, have_stderr, exit_callback)
|
|
if type(pid) == "string" then
|
|
-- Error
|
|
return pid
|
|
end
|
|
|
|
local done_before = false
|
|
local function step_done()
|
|
if have_stdout and have_stderr and not done_before then
|
|
done_before = true
|
|
return
|
|
end
|
|
if done_callback then
|
|
done_callback()
|
|
end
|
|
end
|
|
if have_stdout then
|
|
spawn.read_lines(Gio.UnixInputStream.new(stdout, true),
|
|
stdout_callback, step_done, true)
|
|
end
|
|
if have_stderr then
|
|
spawn.read_lines(Gio.UnixInputStream.new(stderr, true),
|
|
stderr_callback, step_done, true)
|
|
end
|
|
assert(stdin == nil)
|
|
return pid
|
|
end
|
|
|
|
--- Asynchronously spawn a program and capture its output.
|
|
-- (wraps `spawn.with_line_callback`).
|
|
-- @tparam string|table cmd The command.
|
|
-- @tab callback Function with the following arguments
|
|
-- @tparam string callback.stdout Output on stdout.
|
|
-- @tparam string callback.stderr Output on stderr.
|
|
-- @tparam string callback.exitreason Exit Reason.
|
|
-- The reason can be "exit" or "signal".
|
|
-- @tparam integer callback.exitcode Exit code.
|
|
-- For "exit" reason it's the exit code.
|
|
-- For "signal" reason — the signal causing process termination.
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
-- @see spawn.with_line_callback
|
|
function spawn.easy_async(cmd, callback)
|
|
local stdout = ''
|
|
local stderr = ''
|
|
local exitcode, exitreason
|
|
local function parse_stdout(str)
|
|
stdout = stdout .. str .. "\n"
|
|
end
|
|
local function parse_stderr(str)
|
|
stderr = stderr .. str .. "\n"
|
|
end
|
|
local function done_callback()
|
|
return callback(stdout, stderr, exitreason, exitcode)
|
|
end
|
|
local exit_callback_fired = false
|
|
local output_done_callback_fired = false
|
|
local function exit_callback(reason, code)
|
|
exitcode = code
|
|
exitreason = reason
|
|
exit_callback_fired = true
|
|
if output_done_callback_fired then
|
|
return done_callback()
|
|
end
|
|
end
|
|
local function output_done_callback()
|
|
output_done_callback_fired = true
|
|
if exit_callback_fired then
|
|
return done_callback()
|
|
end
|
|
end
|
|
return spawn.with_line_callback(
|
|
cmd, {
|
|
stdout=parse_stdout,
|
|
stderr=parse_stderr,
|
|
exit=exit_callback,
|
|
output_done=output_done_callback
|
|
})
|
|
end
|
|
|
|
--- Read lines from a Gio input stream
|
|
-- @tparam Gio.InputStream input_stream The input stream to read from.
|
|
-- @tparam function line_callback Function that is called with each line
|
|
-- read, e.g. `line_callback(line_from_stream)`.
|
|
-- @tparam[opt] function done_callback Function that is called when the
|
|
-- operation finishes (e.g. due to end of file).
|
|
-- @tparam[opt=false] boolean close Should the stream be closed after end-of-file?
|
|
function spawn.read_lines(input_stream, line_callback, done_callback, close)
|
|
local stream = Gio.DataInputStream.new(input_stream)
|
|
local function done()
|
|
if close then
|
|
stream:close()
|
|
end
|
|
if done_callback then
|
|
protected_call(done_callback)
|
|
end
|
|
end
|
|
local start_read, finish_read
|
|
start_read = function()
|
|
stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read)
|
|
end
|
|
finish_read = function(obj, res)
|
|
local line, length = obj:read_line_finish(res)
|
|
if type(length) ~= "number" then
|
|
-- Error
|
|
print("Error in awful.spawn.read_lines:", tostring(length))
|
|
done()
|
|
elseif end_of_file(line, length) then
|
|
-- End of file
|
|
done()
|
|
else
|
|
-- Read a line
|
|
-- This needs tostring() for older lgi versions which returned
|
|
-- "GLib.Bytes" instead of Lua strings (I guess)
|
|
protected_call(line_callback, tostring(line))
|
|
|
|
-- Read the next line
|
|
start_read()
|
|
end
|
|
end
|
|
start_read()
|
|
end
|
|
|
|
--- Read a program output and return its output as a string.
|
|
-- @tparam string cmd The command to run.
|
|
-- @treturn string A string with the program output, or the error if one
|
|
-- occured.
|
|
function spawn.pread(cmd)
|
|
if cmd and cmd ~= "" then
|
|
local f, err = io.popen(cmd, 'r')
|
|
if f then
|
|
local s = f:read("*all")
|
|
f:close()
|
|
return s
|
|
else
|
|
return err
|
|
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 )
|
|
|
|
capi.client.add_signal ("spawn::completed_with_payload" )
|
|
|
|
return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end })
|
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|