繁体   English   中英

尝试在 gnome shell 扩展中执行多个命令时,UI 会冻结一小会儿

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

原始问题: Gio.Subprocess 中的多个 arguments

所以目前我正在尝试通过 Gio.Subprocess 在我的 gnome-shell-extension 中执行多个异步命令。 如果我将所有命令都作为一个链式命令并在子进程的命令向量中使用&& ,这很好用。 该方案的缺点是,不同链式命令的output只更新一次,执行时间可能较长。

我现在要做的是同时执行每个命令。 现在 output 可以在一个命令的间隔很小而另一个命令需要更多时间的情况下进行更新。

假设这些是我的命令,在这种情况下,我想每秒执行每个命令: 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}]}

然后我为每个命令调用我的刷新function。

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

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

我从调试中发现,似乎是子进程的初始化导致了小冻结,可能是因为所有命令几乎同时使用它?

proc.init(cancellable);

我认为文档说 init 方法是同步的( https://developer.gnome.org/gio//2.56/GInitable.html#g-initable-init )并且似乎还有一个异步版本( https : //developer.gnome.org/gio//2.56/GAsyncInitable.html#g-async-initable-init-async ),但 Gio.Subprocess 只实现同步的( https://developer.gnome.org/ gio//2.56/GSubprocess.html )

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

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

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 的使用做一些评论。

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 用法的说明:

/* 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 使用情况,并更明确地说明您如何使用超时源,因为这些可能每秒都在堆积。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM