简体   繁体   中英

Running an asynchronous function in a GNOME extension

I want to run a loop inside a GNOME extension. after a DBus service method call, but gnome shell freezes

I have learned that extensions run in the GLib's Main Loop and I should use GTask API , but I cannot find a way to use it, nor a clear example.

I think I cannot use GLib.spawn_async_with_pipes because I don't want to call a command, but a function in the same class

Example of code:

 class Foo { constructor () { this._ownName() } _ownName () { Gio.bus_own_name( Gio.BusType.SESSION, 'net.foo', Gio.BusNameOwnerFlags.NONE, this._onBusAcquired.bind(this), this._noOp, this._noOp ) } _onBusAcquired(connection) { connection.register_object( '/net/foo', GioInterface, this._methodCallClosure.bind(this), this._noOp, this._noOp ) _methodCallClosure(connection, sender, objectPath, interfaceName, methodName, parameters, invocation) { this._listen = true this._runLoop() invocation.return_value(null) } // this function should not block GLib's Main Loop _runLoop () { while(this._listen) { ... } } }

There are probably a number ways you can avoid blocking the main loop, but which is best probably depends on what event will result in breaking the while loop.

If you are really in need of "polling" some condition, the simplest approache I think would probably be a timeout loop:

function myPollingFunc(arg1, arg2) {
    if (the_event_occured) {
        // Here is where you will invoke whatever it is you want to do
        // when the event occurs, then destroy the source since the
        // condition has been met.
        this.classMethod(arg1, arg2);

        return GLib.SOURCE_REMOVE;
    }

    // If the condition was not met, you can wait until the next loop and
    // check again.
    return GLib.SOURCE_CONTINUE;
}

// Probably you'll want to store this ID (maybe as a property on your class),
// so you can destroy the source if the DBus service is destroyed before the
// event you're waiting for occurs.
let sourceId;

sourceId = GLib.timeout_add_seconds(
    // Generally this priority is fine, but you may choose another lower one
    // if that makes sense
    GLib.PRIORITY_DEFAULT,

    // The timeout interval of your loop. As described in the linked article,
    // second-based loops tend to be a better choice in terms of performance,
    // however note that at least one interval will pass before the callback
    // is invoked.
    1,

    // Your callback function. Since you're probably creating the source from
    // a class method and intend on modifying some internal state of your class
    // you can bind the function to the class scope, making it's internal state
    // and other methods available to your callback.
    //
    // Function.bind() also allows you to prepend arguments to the callback, if
    // that's necessary or a better choice. As noted above, you'll probably want
    // to store the ID, which is especially important if you bind the callback to
    // `this`.
    // 
    // The reason is that the callback will hold a reference to the object it's
    // bound to (`this` === `Foo`), and the source will hold the callback in the
    // loop. So if you lose track of the source and the ability to destroy it, the
    // object will never be garbage collected.
    myPollingFunc.bind(this, arg1, arg2)
);

You could do the same with an idle source, which waits until no higher priority event is pending rather than a fixed timeout. The catch with idle sources is if there are no other pending events, your callback will be repeatedly invoked almost as fast as a while loop and you'll probably starve out the main loop (eg. make it difficult for other events to get a foot in the door).

On the other hand, there may be a more straight-forward way to do this if you don't actually need to poll a condition, but are waiting for a signal to be emitted or a GObject property to change:

...

  _runLoop () {
    // Waiting for some object to emit a signal or change a property
    let handlerId = someObject.connect('some-signal', () => {
        someObject.disconnect(handlerId);

        this.classMethod(arg1, arg2);
    });

    let propId = someGObject.connect('notify::some-prop', () => {
        if (someGObject.some_prop === 'expected_value') {
            someGObject.disconnect(propId);

            this.classMethod(arg1, arg2);
        }
    });
  }

...

Without knowing what type of event or condition you're waiting for, it's hard to give better advice on the best solution, but maybe this will help. I would say in general if you think you want to loop on some condition, you're better off check that condition in the existing GLib mainloop than creating your own sub-loop.

EDIT

With more context, it seems like you're going to be relying on an external process, one way or the other. In this case, I would strongly recommend avoid the lower-level spawn functions in GLib and instead use GSubprocess.

This is a much safer choice for higher-level language bindings, and also includes helper functions written in C that you'll likely end up rewriting anyways:

onProcExited(proc, result) {
    try {
        proc.wait_check_finish(proc, result);
    } catch (e) {
        logError(e);
    }
}

onLineRead(stdout, result) {
    try {
        let line = stdout.read_line_finish_utf8(result)[0];

        // %null generally means end of stream
        if (line !== null) {
            // Here you can do whatever processing on the line
            // you need to do, and this will be non-blocking as
            // all the I/O was done in a thread.
            someFunc(line);

            // Now you can request the next line
            stdout.read_line_async(null, onLineRead.bind(this));
        }
    } catch (e) {
        logError(e);
    }
}

startFunc() {
    this._proc = new Gio.Subprocess({
        argv: ['proc_name', '--switch', 'arg', 'etc'],
        flags: Gio.SubprocessFlags.STDOUT_PIPE
    });
    this._proc.init(null);

    // Get the stdout pipe and wrap it in a buffered stream
    // with some useful helpers
    let stdout = new Gio.DataInputStream({
        base_stream: this._proc.get_stdout_pipe()
    });

    // This function will spawn dedicated a thread, reading and buffering
    // bytes until it finds a line ending and then invoke onLineRead() in
    // in the main thread.
    stdout.read_line_async(
        GLib.PRIORITY_DEFAULT,
        null // Cancellable, if you want it
        onLineRead.bind(this)
    );

    // Check the process completion
    this._proc.wait_check_async(null, onProcExited.bind(this));
}

Writing a recursive read loop like this is quite straight-forward and the GTask function ( *_async() / *_finish() ) takes care of scheduling the callbacks in the main loop for you. All the I/O is done in a dedicated thread, so the work you do processing the output is all non-blocking.

When you eventually drop your reference to this._proc , you can be assured that all the pipes and resources are cleaned up properly, avoid dangling file descriptors, zombie processes and so on. If you need to exit the process early, you can always call Gio.Subprocess.force_exit() . The read loop itself will hold a reference on the stdout pipe wrapper, so you can just leave it to do its thing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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