Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shutting down a Mojo::IOLoop recurring event connected to a Mojo websocket

I'm playing around with Mojolicious and websockets. I want to send the output of multiple external commands on the server to the webpage. I have no problems with connecting and receiving messages, but I also want to send a message back to the server to stop an external command while letting the others keep sending messages back to the client. I also want to stop checking the external command once it exits.

The external command is simply a one-liner that spits out an integer every few seconds. I have two websockets that display the numbers in separate divs. Clicking either of the stop buttons sends the message, but that's where I need to figure out how to shut down that websocket (and only that websocket) and shut down the external command.

enter image description here

When I connect the websocket, I run the external command and set up a Mojo::IOLoop->recurring to check if there's output.

When I want to stop, I figure that I should call Mojo::IOLoop->remove($id), but that doesn't seem to completely remove it and I get error messages like Mojo::Reactor::Poll: Timer failed: Can't call method "is_websocket" on an undefined value.

If I call finish on the controller object to shut down the websocket, it seems to stop everything.

I have the entire Mojolicious::Lite app as a gist, but here's the parts where I

use feature qw(signatures);
no warnings qw(experimental::signatures);
## other boilerplate redacted

websocket '/find' => sub ( $c ) {
    state $loop = Mojo::IOLoop->singleton;

    app->log->debug( "websocket for find" );
    $c->inactivity_timeout( 50 );

    my $id;
    $c->on( message => sub ( $ws, $message ) {
        my $json = decode_json( $message );
        my $command = $json->{c};
        my $name    = $json->{n};

        app->log->debug( "Got $command command for $name" );
        if( $command eq "start" ) {
            $id = run_command( $ws );
            app->log->debug( "run_command for $name returned [$id]" );
            }
        elsif( $command eq "stop" ) {
            app->log->debug( "stopping loop for $name [$id]" );
            # XXX What should I do here?
            # $ws->finish;
            # $loop->remove( $id );
            }
        elsif( $command eq "open" ) {
            app->log->debug( "opening websocket for $name" );
            }
        }
        );
     $c->on(
        finish => sub ( $c, $code ) {
            app->log->debug("WebSocket closed with status $code");
            }
        );
    };

app->start;

sub run_command ( $ws ) {
    app->log->debug( "In run_command: $ws" );
    open my $fh, "$^X -le '\$|++; while(1) { print int rand(100); sleep 3 }' |";
    $fh->autoflush;

    my $id;
    $id = Mojo::IOLoop->recurring( 1 => sub ($loop) {
        my $m = <$fh>;
        unless( defined $m ) {
            app->log->debug( "Closing down recurring loop from the inside [$id]" );
            # XXX: what should I do here?
            close $fh;
            return;
            };
        chomp $m;
        app->log->debug( "Input [$m] for [$id] from $fh" );
        $ws->send( encode_json( { 'm' => $m } ) );
        });

    return $id;
    }

Other questions that may benefit from this answer:

  • Output command to socket without buffering using Mojo::IOLoop
like image 770
brian d foy Avatar asked Apr 17 '16 06:04

brian d foy


1 Answers

I played around with this a bit. Logioniz's answer made me think that I shouldn't be polling or handling the filehandle details myself. I still don't know where it was hanging.

Instead, I used Mojo::Reactor's io to set a filehandle to monitor:

sub run_command ( $ws ) {
    my $pid = open my $fh, "$^X -le '\$|++; print \$\$; while(1) { print int rand(100); sleep 3 }' |";
    $fh->autoflush;

    my $reactor = Mojo::IOLoop->singleton->reactor->io(
        $fh => sub ($reactor, $writeable) {
            my $m = <$fh>;
            chomp $m;
            $ws->send( encode_json( { 'm' => $m } ) );
            }
        );

    return ( $fh, $pid );
    }

When I'm done with that command, I can unwatch that filehandle and kill the process. I finish the websocket:

    elsif( $command eq "stop" ) {
        $loop->reactor->watch( $fh, 0, 0 );
        kill 'KILL', $pid or app->log->debug( "Could not kill $pid: $!" );
        $ws->finish;
        }

I still don't know why remove($fh) doesn't work. I figure I'm leaking some IOLoop things doing it this way.

like image 168
brian d foy Avatar answered Sep 28 '22 09:09

brian d foy