UI试图在gnome shell扩展中执行多个命令时冻结了片刻

问题描述 投票:0回答:1

原始问题:Multiple arguments in Gio.Subprocess

所以目前,我正在尝试通过Gio.Subprocess在我的gnome-shell-extension中执行多个异步命令。如果我将所有命令仅作为一个链式命令,并且将&&放在子进程的命令向量中,则此方法很好。该解决方案的缺点是,不同链接命令的输出仅更新一次,执行时间可能很长。

我现在想做的是同时执行每个命令。现在,如果一个命令的间隔很小,而另一条命令需要更多时间,则可以更新输出。

假设这些是我的commands,在这种情况下,我想每秒执行一次每个命令:let命令= {“命令”:[{“命令”:“ ls”,“间隔”: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函数。

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

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

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

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

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

Whole extension.js(为了简单起见,输出的最终更新不是此版本的一部分):

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

linux glib gnome gnome-shell gnome-shell-extensions
1个回答
0
投票
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()一次等待它们。用未决的源和承诺重载事件循环也可能是冻结的原因。
© www.soinside.com 2019 - 2024. All rights reserved.