summaryrefslogtreecommitdiff
path: root/lib/awful/spawn.lua
blob: bcfb68dce7bb422cb04cf466e775fb7063903724 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
---------------------------------------------------------------------------
--- 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 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]
        c:emit_signal("spawn::completed_with_payload", props, callback)
        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.
        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.
-- 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

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