Gnome shell extensions: how to run a command with pipes
Asked Answered
N

3

6

So I'm making a Gnome Shell extension. And I want to be able to run some command with a pipe. (The command is actually "xrandr --query | awk 'something'", but that is off topic)

So, what I have done so far is

GLib.spawn_async_with_pipes(null,
                            ['/usr/bin/xrandr', '--query', '|', 'awk...'], null,
                            GLib.SpawnFlags.DO_NOT_REAP_CHILD, null);

But it doesn't work! I can't find any example of running a command in a gnome extensions with a pipe.

Do I have to write "|" in the command like I did ?

Nielsen answered 25/11, 2015 at 8:36 Comment(6)
Can include link to spawn_async_with_pipes documentation ? Tried without commas ['/usr/bin/xrandr --query | awk...'] ?Phonemic
Doc is there Without commas it doesn't works at all.Nielsen
Why not place all your pipes in a bash script, and then use the bash script directly?Agonist
Tried with full path to awk ? " By default, the name of the program must be a full path." valadoc.org/#!api=glib-2.0/GLib.Process.spawn_async_with_pipesPhonemic
You need to call spawn_async_with_pipes twice, once for the xrandr command and once for the awk command. Capture the standard out of the first, and pass that as an argument to the second. You can't run a shell pipeline by using | as an argument.Butterandeggs
Thanks @chepter, I will try that. Do you have any example by the way ?Nielsen
B
3

spawn_async_with_pipes doesn't do what you want (in a simple way). It return the pipes to process with it. You could do it with two call and connecting but it will be a little bit complex.

A simple way to keep the exact syntax is to call a shell which will do the pipe processing with the help of this answer which give a way to call a command, I wrote the following code which call the shell (bash for this case) with correct arguments

const Util = imports.misc.util;
Util.spawn(['/bin/bash', '-c', "xrandr --query | awk 'something'"])
Banshee answered 28/11, 2015 at 17:5 Comment(3)
Great! It worked and should be the accepted answer!Pricilla
Does not work for me, throws either Jun 29 09:23:26 home gnome-shell[3880]: Could not create transient scope for PID 78773: GDBus.Error:org.freedesktop.DBus.Error.UnixProcessIdUnknown: Process with ID 78773 does not exist., Jun 29 09:23:26 home gnome-shell[78773]: /bin/bash: xinput -list: command not found or error: Execution of “xinput -list” failed:: Command not foundAccouchement
this is a different problem which worth a question by itselfBanshee
L
1

I implemented a TerminalReader class some time ago in a Cinnamon Applet: https://github.com/lestcape/Configurable-Menu/blob/OwnAPI/configurableMenu%40lestcape/pakagesManager.js#L31

This class is now used in other places also, so you have more examples to underestand it better: https://github.com/search?l=JavaScript&q=TerminalReader&type=Code&utf8=%E2%9C%93

Here is the source code of the class:

function TerminalReader() {
    this._init.apply(this, arguments);
}

TerminalReader.prototype = {
_init: function(command, callback) {
    this._callbackPipe = callback;
    this._commandPipe = command;
    this.idle = true;
    this._childWatch = null;
},

executeReader: function() {
    if(this.idle) {
        this.idle = false;
        try {
            let [success, argv] = GLib.shell_parse_argv("sh -c '" + this._commandPipe + "'");
            if(success) {
                let [exit, pid, stdin, stdout, stderr] =
                  GLib.spawn_async_with_pipes(
                      null, // cwd
                      argv, // args
                      null, // env
                      GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, //Use env path and no repet
                      null // child_setup
                  );

                this._childPid = pid;
                this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
                this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
                this._stderr = new Gio.UnixInputStream({ fd: stderr, close_fd: true });

                // We need this one too, even if don't actually care of what the process
                // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
                // is kept open indefinitely
                this._stderrStream = new Gio.DataInputStream({ base_stream: this._stderr });
                this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
                this._cancellableStderrStream = new Gio.Cancellable();
                this._cancellableStdout = new Gio.Cancellable();

                this.resOut = 1;
                this._readStdout();
                this.resErr = 1;
                this._readStderror();

                this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, Lang.bind(this, function(pid, status, requestObj) {
                    GLib.source_remove(this._childWatch);
                    this._childWatch = null;
                    this._stdin.close(null);
                    this.idle = true;
                }));
            }
            //throw
        } catch(err) {
            if(err.code == GLib.SpawnError.G_SPAWN_ERROR_NOENT) {
                err.message = _("Command not found.");
            } else {
                // The exception from gjs contains an error string like:
                //   Error invoking GLib.spawn_command_line_async: Failed to
                //   execute child process "foo" (No such file or directory)
                // We are only interested in the part in the parentheses. (And
                // we can't pattern match the text, since it gets localized.)
                err.message = err.message.replace(/.*\((.+)\)/, '$1');
            }
            throw err;
        }
    }
},

destroy: function() {
    try {
        if(this._childWatch) {
            GLib.source_remove(this._childWatch);
            this._childWatch = null;
        }
        if(!this._dataStdout.is_closed()) {
            this._cancellableStdout.cancel();
            this._stdout.close_async(0, null, Lang.bind(this, this.closeStdout));
        }
        if(!this._stderrStream.is_closed()) {
            this._cancellableStderrStream.cancel();
            this._stderrStream.close_async(0, null, Lang.bind(this, this.closeStderrStream));
        }
        this._stdin.close(null);
        this.idle = true;
    }
    catch(e) {
        Main.notify("Error on close" + this._dataStdout.is_closed(), e.message);
    }
},

closeStderrStream: function(std, result) {
    try {
        std.close_finish(result);
    } catch(e) {
        std.close_async(0, null, Lang.bind(this, this.closeStderrStream));
    }
},

closeStdout: function(std, result) {
    try {
        std.close_finish(result);
    } catch(e) {
        std.close_async(0, null, Lang.bind(this, this.closeStderrStream));
    }
},

_readStdout: function() {
    this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, this._cancellableStdout, Lang.bind(this, function(stream, result) {
        try {
            if(!this._dataStdout.is_closed()) {
                if(this.resOut != -1)
                    this.resOut = this._dataStdout.fill_finish(result);// end of file
                if(this.resOut == 0) {
                    let val = stream.peek_buffer().toString();
                    if(val != "")
                        this._callbackPipe(this._commandPipe, true, val);
                    this._stdout.close(this._cancellableStdout);
                } else {
                    // Try to read more
                    this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
                    this._readStdout();
                }
            }
        } catch(e) {
            global.log(e.toString());
        }
    }));
},

_readStderror: function() {
    this._stderrStream.fill_async(-1, GLib.PRIORITY_DEFAULT, this._cancellableStderrStream, Lang.bind(this, function(stream, result) {
        try {
            if(!this._stderrStream.is_closed()) {
                if(this.resErr != -1)
                    this.resErr = this._stderrStream.fill_finish(result);
                if(this.resErr == 0) { // end of file
                    let val = stream.peek_buffer().toString();
                    if(val != "")
                        this._callbackPipe(this._commandPipe, false, val);
                    this._stderr.close(null);
                } else {
                    this._stderrStream.set_buffer_size(2 * this._stderrStream.get_buffer_size());
                    this._readStderror();
                }
            }
        } catch(e) {
            global.log(e.toString());
        }
    }));
}
};
Lotz answered 19/1, 2018 at 7:49 Comment(0)
F
0

calling spawn_async_with_pipes() isn't going to help you, as it gives you back an object with availlable pipes for stdin/stdout/stderr rather than giving you calls that are piped to one another. Except for just calling a shell instance and letting it execute commands, the only way is to stick to the extension itself, letting GNOME handle everything using a temp file and default shell (one that overwrites itself if it already exists).

For the example code, lets do the same as journalctl | grep -i js would do on a CLI:

//Gio for File I/O and GLib for command execution
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
//Create an object for the temp file
let file = Gio.file_new_tmp(null);
//Now write the output of journalctl to the file
file[1].get_output_stream().write(GLib.spawn_command_line_sync("journalctl")[1], null);
//Execute the command and save the result, this locks the thread though,
//so don't use indefinite commands like journalctl -f
//This returns [Boolean success, String stdout, String stderr, Number exit_status]
let journalJS = GLib.spawn_command_line_sync('grep -i js ' + file[0].get_path())[1]

Now feel free to do with the data you have, as everything exits after the command is finished.

Don't forget to close the file using file[1].close(); and setting any remaining variables to null when finished.

Fiscus answered 9/7, 2018 at 10:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.