尝试在gnome shell扩展中执行多个命令时,UI冻结了片刻 [英] UI freezes for a short moment while trying to execute multiple commands in a gnome shell extension

查看:93
本文介绍了尝试在gnome shell扩展中执行多个命令时,UI冻结了片刻的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

原始问题: Gio.Subprocess中有多个参数

因此,当前我正在尝试通过Gio.Subprocess在我的gnome-shell-extension中执行多个异步命令.如果我将所有命令作为一个链式命令放在&& 中,则该方法工作良好,该命令位于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命令= {命令":[{命令":"ls",间隔":1},

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}]}

然后我为每个命令调用 refresh 函数.

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

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

现在发生的是,gnome UI几乎每秒都冻结,而不是冻结,但是即使使用异步通信,我也可以在很短的时间内看到鼠标光标或滚动停止.

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

我认为文档说init方法是同步的(

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)

最后一个问题是,避免冻结的正确方法是什么?我试图将init部分移到异步功能,并在完成后继续通过回调执行命令,但是没有运气.也许这甚至是完全错误的方法.

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.

整个extension.js(为简单起见,输出的最终更新不属于该版本):

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

推荐答案

令人怀疑的是Gio.Initable.init()是导致冻结的原因.首先在这里对GSubprocess的用法进行一些评论.

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

关于Promise/async使用的注意事项:

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

现在执行:

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

很难说是什么原因导致了您遇到的冻结,但是我首先要清理Promise用法,并更明确地说明如何使用超时源,因为这些超时源可能每秒都在堆积.

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.

如果可能,您可能希望将子流程分组为一个超时源,可以使用Promise.all()一次等待它们.用待处理的源和承诺重载事件循环也可能是冻结的原因.

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.

这篇关于尝试在gnome shell扩展中执行多个命令时,UI冻结了片刻的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆