0

Original question: Multiple arguments in Gio.Subprocess

So currently I'm trying to execute multiple asynchronous commands in my gnome-shell-extension via Gio.Subprocess. This works fine, if I put all commands as only one chained command with && in the command vector of the Subprocess. The drawback of this solution is, that the output of the different chained commands is only updated once and the execution time may be long.

What I'm now trying to do, is to execute every command on its own at the same time. Now the output can be updated if one command only has a small interval while another one needs more time.

Let's say these are my commands, in this case I would like to execute each command every second: let commands = {"commands":[{"command":"ls","interval":1},

let commands = {"commands":[{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1}]}

Then I'm calling my refresh function for each command.

commands.commands.forEach(command => {
        this.refresh(command);
    })

What is happening now, is that the gnome UI is freezing almost every second, not much, but I can see my mouse cursor or scrolling stop for a very short amount of time, even though I use asynchronous communication.

What I have found out from debugging is that it seems to be the initialization of the Subprocess which causes the small freeze, maybe because all the commands are using it nearly at the same time?

proc.init(cancellable);

I think the documentation says that the init method is synchronous (https://developer.gnome.org/gio//2.56/GInitable.html#g-initable-init) and that there also seems to be an async version (https://developer.gnome.org/gio//2.56/GAsyncInitable.html#g-async-initable-init-async), but the Gio.Subprocess does only implement the synchronous one (https://developer.gnome.org/gio//2.56/GSubprocess.html)

So the final question is, what would be the correct way to avoid the freezing? I tried to move the init part to asynchronous function and continue with the command execution via callbacks after it is done, but with no luck. Maybe this is even the completely wrong approach though.

Whole extension.js (final updating of the output is not part of this version, just for simplicity):

const Main = imports.ui.main;
const GLib = imports.gi.GLib;
const Mainloop = imports.mainloop;
const Gio = imports.gi.Gio;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();

let output, box, gschema, stopped;
var settings;

let commands = {"commands":[{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1},
{"command":"ls","interval":1}]}

function init() { 
    //nothing todo here
}

function enable() {
    stopped = false;

    gschema = Gio.SettingsSchemaSource.new_from_directory(
        Me.dir.get_child('schemas').get_path(),
        Gio.SettingsSchemaSource.get_default(),
        false
    );

    settings = new Gio.Settings({
        settings_schema: gschema.lookup('org.gnome.shell.extensions.executor', true)
    });

    box = new St.BoxLayout({ style_class: 'panel-button' });
    output = new St.Label();    
    box.add(output, {y_fill: false, y_align: St.Align.MIDDLE});
    Main.panel._rightBox.insert_child_at_index(box, 0);

    commands.commands.forEach(command => {
        this.refresh(command);
    })
}

function disable() {
    stopped = true;
    log("Executor stopped");
    Main.panel._rightBox.remove_child(box);
}

async function refresh(command) {
    await this.updateGui(command);

    Mainloop.timeout_add_seconds(command.interval, () => {
        if (!stopped) {
            this.refresh(command);
        }    
    });
}

async function updateGui(command) {
    await execCommand(['/bin/sh', '-c', command.command]).then(stdout => {
        if (stdout) {
            let entries = [];
            stdout.split('\n').map(line => entries.push(line));
            let outputAsOneLine = '';
            entries.forEach(output => {
                outputAsOneLine = outputAsOneLine + output + ' ';
            });
            if (!stopped) {
                log(outputAsOneLine);
                //output.set_text(outputAsOneLine);
            }   
        }
    });
}

async function execCommand(argv, input = null, cancellable = null) {
    try {
        let flags = Gio.SubprocessFlags.STDOUT_PIPE;

        if (input !== null)
            flags |= Gio.SubprocessFlags.STDIN_PIPE;

        let proc = new Gio.Subprocess({
            argv: argv,
            flags: flags
        });

        proc.init(cancellable);

        let stdout = await new Promise((resolve, reject) => {
            proc.communicate_utf8_async(input, cancellable, (proc, res) => {
                try {
                    let [ok, stdout, stderr] = proc.communicate_utf8_finish(res);
                    resolve(stdout);
                } catch (e) {
                    reject(e);
                }
            });
        });

        return stdout;
    } catch (e) {
        logError(e);
    }
}```

raujonas
  • 117
  • 8

1 Answers1

3

It's doubtful that Gio.Initable.init() is what's causing the freeze. First some comments on the usage of GSubprocess here.

function execCommand(argv, input = null, cancellable = null) {
    try {
        /* If you expect to get output from stderr, you need to open
         * that pipe as well, otherwise you will just get `null`. */
        let flags = (Gio.SubprocessFlags.STDOUT_PIPE |
                     Gio.SubprocessFlags.STDERR_PIPE);

        if (input !== null)
            flags |= Gio.SubprocessFlags.STDIN_PIPE;

        /* Using `new` with an initable class like this is only really
         * necessary if it's possible you might pass a pre-triggered
         * cancellable, so you can call `init()` manually.
         *
         * Otherwise you can just use `Gio.Subprocess.new()` which will
         * do exactly the same thing for you, just in a single call
         * without a cancellable argument. */
        let proc = new Gio.Subprocess({
            argv: argv,
            flags: flags
        });
        proc.init(cancellable);

        /* If you want to actually quit the process when the cancellable
         * is triggered, you need to connect to the `cancel` signal */
        if (cancellable instanceof Gio.Cancellable)
            cancellable.connect(() => proc.force_exit());

        /* Remember the process start running as soon as we called
         * `init()`, so this is just the threaded call to read the
         * processes's output.
         */
        return new Promise((resolve, reject) => {
            proc.communicate_utf8_async(input, cancellable, (proc, res) => {
                try {
                    let [, stdout, stderr] = proc.communicate_utf8_finish(res);

                    /* If you do opt for stderr output, you might as
                     * well use it for more informative errors */
                    if (!proc.get_successful()) {
                        let status = proc.get_exit_status();

                        throw new Gio.IOErrorEnum({
                            code: Gio.io_error_from_errno(status),
                            message: stderr ? stderr.trim() : GLib.strerror(status)
                        });
                    }

                    resolve(stdout);
                } catch (e) {
                    reject(e);
                }
            });
        });

    /* This should only happen if you passed a pre-triggered cancellable
     * or the process legitimately failed to start (eg. commmand not found) */
    } catch (e) {
        return Promise.reject(e);
    }
}

And notes on Promise/async usage:

/* Don't do this. You're effectively mixing two usage patterns
 * of Promises, and still not catching errors. Expect this to
 * blow up in your face long after you expect it to. */
async function foo() {
    await execCommand(['ls']).then(stdout => log(stdout));
}

/* If you're using `await` in an `async` function that is
 * intended to run by itself, you need to catch errors like
 * regular synchronous code */
async function bar() {
    try {
        // The function will "await" the first Promise to
        // resolve successfully before executing the second
        await execCommand(['ls']);
        await execCommand(['ls']);
    } catch (e) {
        logError(e);
    }
}

/* If you're using Promises in the traditional manner, you
 * must catch them that way as well */
function baz() {
    // The function will NOT wait for the first to complete
    // before starting the second. Since these are (basically)
    // running in threads, they are truly running in parallel.
    execCommand(['ls']).then(stdout => {
        log(stdout);
    }).catch(error => {
        logError(error);
    });

    execCommand(['ls']).then(stdout => {
        log(stdout);
    }).catch(error => {
        logError(error);
    });
}

Now for the implementation:

const Main = imports.ui.main;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


let cancellable = null;
let panelBox = null;


let commands = {
    "commands":[
        {"command":"ls","interval":1},
        {"command":"ls","interval":1},
        {"command":"ls","interval":1},
        {"command":"ls","interval":1},
        {"command":"ls","interval":1},
        {"command":"ls","interval":1},
        {"command":"ls","interval":1}
    ]
};

enable() {
    if (cancellable === null)
        cancellable = new Gio.Cancellable();

    panelBox = new St.BoxLayout({
        style_class: 'panel-button'
    });

    // Avoid deprecated methods like `add()`, and try not
    // to use global variable when possible
    let outputLabel = new St.Label({
        y_align: St.Align.MIDDLE,
        y_fill: false
    });
    panelBox.add_child(outputLabel);

    Main.panel._rightBox.insert_child_at_index(panelBox, 0);

    commands.commands.forEach(command => {
        this.refresh(command);
    });
}

disable() {
    if (cancellable !== null) {
        cancellable.cancel();
        cancellable = null;
    }

    log("Executor stopped");

    if (panelBox !== null) {
        Main.panel._rightBox.remove_child(panelBox);
        panelBox = null;
    }
}

async function refresh(command) {
    try {
        await this.updateGui(command);

        // Don't use MainLoop anymore, just use GLib directly
        GLib.timeout_add_seconds(0, command.interval, () => {
            if (cancellable && !cancellable.is_cancelled())
                this.refresh(command);

            // Always explicitly return false (or this constant)
            // unless you're storing the returned ID to remove the
            // source later.
            //
            // Returning true (GLib.SOURCE_CONTINUE) or a value that
            // evaluates to true will cause the source to loop. You
            // could refactor your code to take advantage of that
            // instead of constantly creating new timeouts each
            // second.
            return GLib.SOURCE_REMOVE;
        });
    } catch (e) {
        // We can skip logging cancelled errors, since we probably
        // did that on purpose if it happens
        if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)
            logError(e, 'Failed to refresh');
    }
}

// `updateGui()` is wrapped in a try...catch above so it's safe to
// skip that here.
async function updateGui(command) {
    let stdout = await execCommand(['/bin/sh', '-c', command.command]);

    // This will probably always be true if the above doesn't throw,
    // but you can check if you want to.
    if (stdout) {
        let outputAsOneLine = stdout.replace('\n', '');

        // No need to check the cancellable here, if it's
        // triggered the command will fail and throw an error
        log(outputAsOneLine);
        // let outputLabel = panelBox.get_first_child();
        // outputLabel.set_text(outputAsOneLine);   
    }
}

It's hard to say what is causing the freeze you are experiencing, but I would first cleanup your Promise usage and be more explicit about how you use timeout sources, as these may be stacking every second.

If possible, you might want to group your subprocesses into a single timeout source, possible using Promise.all() to await them all at once. Overloading the event loop with pending sources and Promises could also be the cause of the freeze.

andy.holmes
  • 3,383
  • 17
  • 28
  • Thank you very much for your detailed explanation, especially about the correct promise usage! I cleaned it up now but I'm still facing the same issue. To be more explicit about my desired timeout usage: It's totally ok that the "new" timeout is started after the moment the result is there (with `await this.updateGui(command);` I'm awaiting the output here before setting a new interval, so I think that the timeouts should not stack). I want to execute all commands separately so that I can update the output later on as soon as the latest executed result is there. – raujonas May 13 '20 at 20:07
  • This is why I think Promise.all() to await them all at once would not help me, right? Of course the user could specify different intervals for each command (e.g. 2,5,13,17, ..). In this case or if you have only 1 command the freeze is not a problem, it seems the execution is more spreaded. An example would be system monitoring: Getting the actual cpu usage is faster than getting the current network utilization in my case, so I would like to update the cpu value without waiting for the network value, and update the network value later when it arrives, starting each again after completion. – raujonas May 13 '20 at 20:13
  • But if I am not using the Subprocess in my execCommand function and just resolve a string in the promise or comment out the initialization, the lag does not occur. This is why I first thought the problem could be here if my commands do arrive here nearly at the same time when they have the same execution time and same interval. – raujonas May 13 '20 at 20:14
  • I'm still not sure GSubprocess is the problem, although you can try skipping the `new` usage and use `Gio.Subprocess.new()`. What happens if you replace `0` with `300` (loop priority) in the timeout? – andy.holmes May 14 '20 at 18:13
  • Thank you for answering again! Ok, I tried with `let proc = Gio.Subprocess.new(argv,flags);` and a loop priority of `300`, but it is still freezing :( Here the complete code: https://jsfiddle.net/tschoninas/10Lh86a4/ Edit: Formatting – raujonas May 14 '20 at 19:26
  • There does seem to be a bit of lag, even if I drop the priority to 1000, though it might be worse for you. A simple run through sysprof shows that the big offender is the `fork()` system call, which is basically unavoidable here. I guess your best bet is to maybe loop through your commands to ensure they're not bunching up and all hitting the CPU at the same time. – andy.holmes May 15 '20 at 00:01
  • By loop you mean add a real delay? I tried with 50ms offset between each command with `GLib.timeout_add` (line 91) and it looks good! I store the commands in an array now and check it every second, do you think this could lead to a performance issue or would this be a good approach? Here is the current version (line 75 is what I'm not sure about): https://jsfiddle.net/tschoninas/uf962j5k/3/ Thank you again for helping! – raujonas May 17 '20 at 15:29
  • That's along the line I was thinking yes. I can't give any specific advice on the implementation of your queue, but I think you're on the right track. You might want to drop the priority of the GSource to help ensure your commands are not being prioritized over input/animation events, too. – andy.holmes May 18 '20 at 17:54