mirror of
https://github.com/awesomeWM/awesome
synced 2024-11-17 07:47:41 +01:00
13984af8aa
I do not know how a GDataInputStream / GBufferedInputStream decides about the size of its internal buffer when reading input by line, but in issue #2288, an example where the output of date (about 30 bytes) was read ten times per second caused ten megabytes of memory usage for this internal buffer. Try to save some memory by explicitly shrinking the buffer size when we are done reading from the stream. Reference: https://github.com/awesomeWM/awesome/issues/2288 Signed-off-by: Uli Schlachter <psychon@znc.in>
443 lines
17 KiB
Lua
443 lines
17 KiB
Lua
--luacheck: no max line length
|
|
|
|
---------------------------------------------------------------------------
|
|
--- 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.
|
|
--
|
|
-- **Rules of thumb when a shell is needed**:
|
|
--
|
|
-- * A shell is required when the commands contain `&&`, `;`, `||`, `&` or
|
|
-- any other unix shell language syntax
|
|
-- * When shell variables are defined as part of the command
|
|
-- * When the command is a shell alias
|
|
--
|
|
-- Note that a shell is **not** a terminal emulator. A terminal emulator is
|
|
-- something like XTerm, Gnome-terminal or Konsole. A shell is something like
|
|
-- `bash`, `zsh`, `busybox sh` or `Debian ash`.
|
|
--
|
|
-- If you wish to open a process in a terminal window, check that your terminal
|
|
-- emulator supports the common `-e` option. If it does, then something like
|
|
-- this should work:
|
|
--
|
|
-- awful.spawn(terminal.." -e my_command")
|
|
--
|
|
-- Note that some terminals, such as rxvt-unicode (urxvt) support full commands
|
|
-- using quotes, while other terminal emulators require to use quoting.
|
|
--
|
|
-- **Understanding clients versus PID versus commands versus class**:
|
|
--
|
|
-- A *process* has a *PID* (process identifier). It can have 0, 1 or many
|
|
-- *window*s.
|
|
--
|
|
-- A *command* if what is used to start *process*(es). It has no direct relation
|
|
-- with *process*, *client* or *window*. When a command is executed, it will
|
|
-- usually start a *process* which keeps running until it exits. This however is
|
|
-- not always the case as some applications use scripts as command and others
|
|
-- use various single-instance mechanisms (usually client/server) and merge
|
|
-- with an existing process.
|
|
--
|
|
-- A *client* corresponds to a *window*. It is owned by a process. It can have
|
|
-- both a parent and one or many children. A *client* has a *class*, an
|
|
-- *instance*, a *role*, and a *type*. See `client.class`, `client.instance`,
|
|
-- `client.role` and `client.type` for more information about these properties.
|
|
--
|
|
-- **The startup notification protocol**:
|
|
--
|
|
-- The startup notification protocol is an optional specification implemented
|
|
-- by X11 applications to bridge the chain of knowledge between the moment a
|
|
-- program is launched to the moment its window (client) is shown. It can be
|
|
-- found [on the FreeDesktop.org website](https://www.freedesktop.org/wiki/Specifications/startup-notification-spec/).
|
|
--
|
|
-- Awesome has support for the various events that are part of the protocol, but
|
|
-- the most useful is the identifier, usually identified by its `SNID` acronym in
|
|
-- the documentation. It isn't usually necessary to even know it exists, as it
|
|
-- is all done automatically. However, if more control is required, the
|
|
-- identifier can be specified by an environment variable called
|
|
-- `DESKTOP_STARTUP_ID`. For example, let us consider execution of the following
|
|
-- command:
|
|
--
|
|
-- DESKTOP_STARTUP_ID="something_TIME$(date '+%s')" my_command
|
|
--
|
|
-- This should (if the program correctly implements the protocol) result in
|
|
-- `c.startup_id` to at least match `something`.
|
|
-- This identifier can then be used in `awful.rules` to configure the client.
|
|
--
|
|
-- Awesome can automatically set the `DESKTOP_STARTUP_ID` variable. This is used
|
|
-- by `awful.spawn` to specify additional rules for the startup. For example:
|
|
--
|
|
-- awful.spawn("urxvt -e maxima -name CALCULATOR", {
|
|
-- floating = true,
|
|
-- tag = mouse.screen.selected_tag,
|
|
-- placement = awful.placement.bottom_right,
|
|
-- })
|
|
--
|
|
-- This can also be used from the command line:
|
|
--
|
|
-- awesome-client 'awful=require("awful");
|
|
-- awful.spawn("urxvt -e maxima -name CALCULATOR", {
|
|
-- floating = true,
|
|
-- tag = mouse.screen.selected_tag,
|
|
-- placement = awful.placement.bottom_right,
|
|
-- })'
|
|
--
|
|
-- **Getting a command's output**:
|
|
--
|
|
-- First, do **not** use `io.popen` **ever**. It is synchronous. Synchronous
|
|
-- functions **block everything** until they are done. All visual applications
|
|
-- lock (as Awesome no longer responds), you will probably lose some keyboard
|
|
-- and mouse events and will have higher latency when playing games. This is
|
|
-- also true when reading files synchronously, but this is another topic.
|
|
--
|
|
-- Awesome provides a few ways to get output from commands. One is to use the
|
|
-- `Gio` libraries directly. This is usually very complicated, but gives a lot
|
|
-- of control on the command execution.
|
|
--
|
|
-- This modules provides `with_line_callback` and `easy_async` for convenience.
|
|
-- First, lets add this bash command to `rc.lua`:
|
|
--
|
|
-- local noisy = [[bash -c '
|
|
-- for I in $(seq 1 5); do
|
|
-- date
|
|
-- echo err >&2
|
|
-- sleep 2
|
|
-- done
|
|
-- ']]
|
|
--
|
|
-- It prints a bunch of junk on the standard output (*stdout*) and error
|
|
-- (*stderr*) streams. This command would block Awesome for 10 seconds if it
|
|
-- were executed synchronously, but will not block it at all using the
|
|
-- asynchronous functions.
|
|
--
|
|
-- `with_line_callback` will execute the callbacks every time a new line is
|
|
-- printed by the command:
|
|
--
|
|
-- awful.spawn.with_line_callback(noisy, {
|
|
-- stdout = function(line)
|
|
-- naughty.notify { text = "LINE:"..line }
|
|
-- end,
|
|
-- stderr = function(line)
|
|
-- naughty.notify { text = "ERR:"..line}
|
|
-- end,
|
|
-- })
|
|
--
|
|
-- If only the full output is needed, then `easy_async` is the right choice:
|
|
--
|
|
-- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code)
|
|
-- naughty.notify { text = stdout }
|
|
-- end)
|
|
--
|
|
-- **Default applications**:
|
|
--
|
|
-- If the intent is to open a file/document, then it is recommended to use the
|
|
-- following standard command. The default application will be selected
|
|
-- according to the [Shared MIME-info Database](https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html)
|
|
-- specification. The `xdg-utils` package provided by most distributions
|
|
-- includes the `xdg-open` command:
|
|
--
|
|
-- awful.spawn({"xdg-open", "/path/to/file"})
|
|
--
|
|
-- Awesome **does not** manage, modify or otherwise influence the database
|
|
-- for default applications. For information about how to do this, consult the
|
|
-- [ARCH Linux Wiki](https://wiki.archlinux.org/index.php/default_applications).
|
|
--
|
|
-- If you wish to change how the default applications behave, then consult the
|
|
-- [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html)
|
|
-- specification.
|
|
--
|
|
-- @author Julien Danjou <julien@danjou.info>
|
|
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
|
|
-- @copyright 2008 Julien Danjou
|
|
-- @copyright 2014 Emmanuel Lepage Vallee
|
|
-- @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 timer = require("gears.timer")
|
|
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 entry = spawn.snid_buffer[c.startup_id]
|
|
if entry then
|
|
local props = entry[1]
|
|
local callback = entry[2]
|
|
--TODO v5: Remove this signal
|
|
c:emit_signal("spawn::completed_with_payload", props, callback)
|
|
|
|
timer.delayed_call(function()
|
|
spawn.snid_buffer[c.startup_id] = nil
|
|
end)
|
|
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.
|
|
local pid, snid = capi.awesome.spawn(cmd, enable_sn)
|
|
-- The snid will be nil in case of failure
|
|
if snid then
|
|
sn_rules = type(sn_rules) ~= "boolean" and sn_rules or {}
|
|
spawn.snid_buffer[snid] = { sn_rules, { callback } }
|
|
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 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
|
|
-- arguments.
|
|
-- 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 ("exit" or "signal").
|
|
-- @tparam integer callback.exitcode Exit code (exit code or signal number,
|
|
-- depending on "exitreason").
|
|
-- @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
|
|
|
|
--- Call `spawn.easy_async` with a shell.
|
|
-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`).
|
|
-- @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 ("exit" or "signal").
|
|
-- @tparam integer callback.exitcode Exit code (exit code or signal number,
|
|
-- depending on "exitreason").
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
-- @see spawn.with_line_callback
|
|
function spawn.easy_async_with_shell(cmd, callback)
|
|
return spawn.easy_async({ util.shell, "-c", cmd or "" }, 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
|
|
stream:set_buffer_size(0)
|
|
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
|
|
|
|
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 )
|
|
|
|
return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end })
|
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|