UI freezes for a short moment while trying to execute multiple commands in a gnome shell extension

為{幸葍}努か 提交于 2020-05-17 06:11:07

问题


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);
    }
}```


回答1:


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.



来源:https://stackoverflow.com/questions/61760615/ui-freezes-for-a-short-moment-while-trying-to-execute-multiple-commands-in-a-gno

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!