From 2d8bf89ff9e87fc05c52b6c05c3cbe0ff9bb8644 Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Thu, 13 Aug 2015 09:05:52 +0200 Subject: [PATCH 1/5] awesome.spawn(): Add possibility to return pipes This adds three new arguments to awesome.spawn() that tell it to return pipes for stdin, stdout and stderr of the spawned process. Signed-off-by: Uli Schlachter --- spawn.c | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/spawn.c b/spawn.c index 8153ed38..2bba63e2 100644 --- a/spawn.c +++ b/spawn.c @@ -340,8 +340,14 @@ parse_command(lua_State *L, int idx, GError **error) * * @tparam string|table cmd The command to launch. * @tparam[opt=true] boolean use_sn Use startup-notification? + * @tparam[opt=false] boolean stdin Return a fd for stdin? + * @tparam[opt=false] boolean stdout Return a fd for stdout? + * @tparam[opt=false] boolean stderr Return a fd for stderr? * @treturn[1] integer Process ID if everything is OK. * @treturn[1] string Startup-notification ID, if `use_sn` is true. + * @treturn[1] integer stdin, if `stdin` is true. + * @treturn[1] integer stdout, if `stdout` is true. + * @treturn[1] integer stderr, if `stderr` is true. * @treturn[2] string An error string if an error occured. * @function spawn */ @@ -349,12 +355,26 @@ int luaA_spawn(lua_State *L) { gchar **argv = NULL; - bool use_sn = true; + bool use_sn = true, return_stdin = false, return_stdout = false, return_stderr = false; + int stdin_fd = -1, stdout_fd = -1, stderr_fd = -1; + int *stdin_ptr = NULL, *stdout_ptr = NULL, *stderr_ptr = NULL; gboolean retval; GPid pid; if(lua_gettop(L) >= 2) use_sn = luaA_checkboolean(L, 2); + if(lua_gettop(L) >= 3) + return_stdin = luaA_checkboolean(L, 3); + if(lua_gettop(L) >= 4) + return_stdout = luaA_checkboolean(L, 4); + if(lua_gettop(L) >= 5) + return_stderr = luaA_checkboolean(L, 5); + if(return_stdin) + stdin_ptr = &stdin_fd; + if(return_stdout) + stdout_ptr = &stdout_fd; + if(return_stderr) + stderr_ptr = &stderr_fd; GError *error = NULL; argv = parse_command(L, 1, &error); @@ -383,8 +403,9 @@ luaA_spawn(lua_State *L) sn_launcher_context_setup_child_process(context); } - retval = g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, - spawn_callback, NULL, &pid, &error); + retval = g_spawn_async_with_pipes(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, + spawn_callback, NULL, &pid, + stdin_ptr, stdout_ptr, stderr_ptr, &error); g_strfreev(argv); if(!retval) { @@ -402,9 +423,24 @@ luaA_spawn(lua_State *L) /* push sn on stack */ if (context) - lua_pushstring(L,sn_launcher_context_get_startup_id(context)); + lua_pushstring(L, sn_launcher_context_get_startup_id(context)); + else + lua_pushnil(L); - return (context)?2:1; + if(return_stdin) + lua_pushinteger(L, stdin_fd); + else + lua_pushnil(L); + if(return_stdout) + lua_pushinteger(L, stdout_fd); + else + lua_pushnil(L); + if(return_stderr) + lua_pushinteger(L, stderr_fd); + else + lua_pushnil(L); + + return 5; } // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 From 953cdf416e3296f25c69bb7b1293b29ec39b66ba Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Thu, 13 Aug 2015 09:09:14 +0200 Subject: [PATCH 2/5] lgi-check.sh: Add dependencies to GLib and Gio GLib is already used in three part of awesome (awful.widget.textclock, gears.timer and menubar.utils). Gio will soon be added. Signed-off-by: Uli Schlachter --- build-utils/lgi-check.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-utils/lgi-check.sh b/build-utils/lgi-check.sh index ffabe732..0f7ad78e 100755 --- a/build-utils/lgi-check.sh +++ b/build-utils/lgi-check.sh @@ -32,4 +32,4 @@ lua -e '_, _, major_minor, patch = string.find(require("lgi.version"), "^(%d%.%d "0.7.1", require("lgi.version"))) end' || die # Check for the needed gi files -lua -e 'l = require("lgi") assert(l.cairo, l.Pango, l.PangoCairo)' || die +lua -e 'l = require("lgi") assert(l.cairo, l.Pango, l.PangoCairo, l.GLib, l.Gio)' || die From 1beda989e589469a84f6bce085ff94c33cb87020 Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Thu, 13 Aug 2015 10:57:29 +0200 Subject: [PATCH 3/5] tests/_runner.lua: Load all dependencies This tried to use awful, but didn't load awful itself. Kids, this is why you should make your variables local! Signed-off-by: Uli Schlachter --- tests/_runner.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/_runner.lua b/tests/_runner.lua index c846a922..dce58a14 100644 --- a/tests/_runner.lua +++ b/tests/_runner.lua @@ -1,4 +1,5 @@ -timer = require("gears.timer") +local timer = require("gears.timer") +local awful = require("awful") runner = { quit_awesome_on_error = os.getenv('TEST_PAUSE_ON_ERRORS') ~= '1', From 0e20fef2bde2ee5d0a40d3466d83bf4aef866807 Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Wed, 2 Sep 2015 22:18:33 +0200 Subject: [PATCH 4/5] Add awful.util.spawn_with_line_callback This new function spawns a program, similarly to awful.spawn, but captures its output. On each line of output on stdout / stderr, a Lua function is called with this line. There are different callbacks for stdout and stderr. When both stdout and stderr are closed, another callback function is called. The intention for this last callback is "the program is done", because most programs should only close their output when they exit. Signed-off-by: Uli Schlachter --- lib/awful/util.lua | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/lib/awful/util.lua b/lib/awful/util.lua index ab157171..28087899 100644 --- a/lib/awful/util.lua +++ b/lib/awful/util.lua @@ -22,6 +22,8 @@ local pairs = pairs local string = string local lgi = require("lgi") local Pango = lgi.Pango +local Gio = lgi.Gio +local GLib = lgi.GLib local capi = { awesome = awesome, @@ -110,6 +112,94 @@ function util.spawn_with_shell(cmd, sn) end end +--- Spawn a program and asynchronously capture its output line by line. +-- @tparam string|table cmd The command. +-- @tparam[opt] function stdout_callback Function that is called with each line of +-- output on stdout, e.g. `stdout_callback(line)`. +-- @tparam[opt] function stderr_callback Function that is called with each line of +-- output on stderr, e.g. `stderr_callback(line)`. +-- @tparam[opt] function done_callback Function to call when no more output is +-- produced. +-- @treturn[1] Integer the PID of the forked process. +-- @treturn[2] string Error message. +function util.spawn_with_line_callback(cmd, stdout_callback, stderr_callback, done_callback) + local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil + local pid, sn_id, stdin, stdout, stderr = capi.awesome.spawn(cmd, false, false, have_stdout, have_stderr) + 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 + done_callback() + end + if have_stdout then + util.read_lines(Gio.UnixInputStream.new(stdout, true), + stdout_callback, step_done, true) + end + if have_stderr then + util.read_lines(Gio.UnixInputStream.new(stderr, true), + stderr_callback, step_done, true) + end + assert(stdin == nil) +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 util.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 + xpcall(done_callback, function(err) + print(debug.traceback("Error while calling done_callback:" + .. tostring(err), 2)) + end) + 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.util.read_lines:", tostring(length)) + done() + elseif #line ~= length then + -- End of file + done() + else + -- Read a line + xpcall(function() + -- This needs tostring() for older lgi versions which returned + -- "GLib.Bytes" instead of Lua strings (I guess) + line_callback(tostring(line)) + end, function(err) + print(debug.traceback("Error while calling line_callback: " + .. tostring(err), 2)) + end) + + -- Read the next line + start_read() + end + end + start_read() +end + --- Read a program output and returns its output as a string. -- @param cmd The command to run. -- @return A string with the program output, or the error if one occured. From 3c4a80ca7e9500a57ebdac9bb0cf84ff8a363e12 Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Thu, 13 Aug 2015 11:00:12 +0200 Subject: [PATCH 5/5] Add a test for awful.util.spawn_with_line_callback Signed-off-by: Uli Schlachter --- tests/test-spawn.lua | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test-spawn.lua diff --git a/tests/test-spawn.lua b/tests/test-spawn.lua new file mode 100644 index 00000000..90ae535e --- /dev/null +++ b/tests/test-spawn.lua @@ -0,0 +1,45 @@ +--- Tests for spawn + +local util = require("awful.util") + +local spawns_done = 0 + +local steps = { + function(count) + if count == 1 then + local steps_yay = 0 + util.spawn_with_line_callback("echo yay", function(line) + assert(line == "yay", "line == '" .. tostring(line) .. "'") + assert(steps_yay == 0) + steps_yay = steps_yay + 1 + end, nil, function() + assert(steps_yay == 1) + steps_yay = steps_yay + 1 + spawns_done = spawns_done + 1 + end) + + local steps_count = 0 + local err_count = 0 + util.spawn_with_line_callback({ "sh", "-c", "printf line1\\\\nline2\\\\nline3 ; echo err >&2" }, + function(line) + assert(steps_count < 3) + steps_count = steps_count + 1 + assert(line == "line" .. steps_count, "line == '" .. tostring(line) .. "'") + end, function(line) + assert(err_count == 0) + err_count = err_count + 1 + assert(line == "err", "line == '" .. tostring(line) .. "'") + end, function() + assert(steps_count == 3) + assert(err_count == 1) + steps_count = steps_count + 1 + spawns_done = spawns_done + 1 + end) + end + if spawns_done == 2 then + return true + end + end, +} + +require("_runner").run_steps(steps)