planetteamspeak / ts3phpframework

Modern use-at-will framework that provides individual components to manage TeamSpeak 3 Server instances
https://www.planetteamspeak.com
GNU General Public License v3.0
212 stars 59 forks source link

Callback never gets triggered: connection to server lost #187

Closed Sebbo94BY closed 1 year ago

Sebbo94BY commented 1 year ago

Hey,

I'm currently trying to get a bot connected, but it always looses the connection to the server, although I've configured the known keep-alive logic. It seems like as the keep-alive callback function never gets executed, because I don't see any debug messages during the entire 5 minutes until the timeout occurs.

Calling a different function within the same class works fine (see my below code and the execution).

The plan is to keep the client connected forever (until I kill the PHP process), regulary run specific ServerQuery commands and in case of clients joined or left, the ServerQuery commands should also get executed.

My Environment:

My code:

<?php

namespace App\Console\Commands;

use App\Models\Instance;
use Illuminate\Console\Command;
use PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Signal;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;

class StartBot extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'bot:start';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Starts a TeamSpeak bot.';

    /**
     * Callback method for 'serverqueryWaitTimeout' signals.
     *
     * @param integer $seconds
     * @return void
     */
    public function onWaitTimeout(int $idle_seconds, ServerQuery $serverquery)
    {
        $this->warn("onWaitTimeout triggered");

        if ($idle_seconds % 10 == 0) {
            $this->info("No reply from the server for $idle_seconds seconds");
        }

        if ($serverquery->getQueryLastTimestamp() < time()-250)
        {
            $this->info("Sending keep-alive");

            $serverquery->request("clientupdate");
        }
    }

    /**
     * Just a test function for testing callbacks in general.
     */
    public function test(Instance $instance)
    {
        $this->warn("Callback worked. Virtualserver Port: ".$instance->voice_port);
    }

    /**
     * Starts the bot.
     */
    protected function start_bot(Instance $instance)
    {
        call_user_func([$this, 'test'], $instance);

        $this->info("Starting TeamSpeak bot instance: ".$instance->voice_port);

        $TS3PHPFramework = new TeamSpeak3();

        $connection_uri = "serverquery://$instance->serverquery_username:$instance->serverquery_password@$instance->host:$instance->serverquery_port/?server_port=$instance->voice_port&nickname=$instance->client_nickname&ssh=1&blocking=0";

        try {
            $virtualserver = $TS3PHPFramework->factory($connection_uri);
        } catch (TransportException $e) {
            throw new TransportException($e->getMessage(), $e->getCode());
        }

        if (! is_null($instance->default_channel_id)) {
            try {
                $virtualserver->clientMove($virtualserver->whoamiGet("client_id"), $instance->default_channel_id);
            } catch (ServerQueryException $e) {
                throw new ServerQueryException($e->getMessage(), $e->getCode());
            }
        }

        // get notified on server events
        $virtualserver->notifyRegister("server");

        // register a callback for serverqueryWaitTimeout events 
        Signal::getInstance()->subscribe("serverqueryWaitTimeout", [$this, "onWaitTimeout"]);

        // wait for events
        while (true) $virtualserver->getAdapter()->wait();
    }

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $instance = Instance::all()->first();

        try {
            $this->start_bot($instance);
        } catch (TransportException $transport_exception) {
            $this->error($transport_exception->getMessage());
        }
    }
}

As you can see in the below output: I never see the onWaitTimeout triggered text, which should get always printed, when the callback gets triggered.

www-data@e33647dcf607:~$ php artisan bot:start
Callback worked. Virtualserver Port: 9987
Starting TeamSpeak bot instance: 9987
connection to server 'localhost:10022' lost

Exception:

   PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException 

  connection to server 'localhost:10022' lost

  at vendor/planetteamspeak/ts3-php-framework/src/Transport/TCP.php:162
    158▕             if ($data === false) {
    159▕                 if ($line->count()) {
    160▕                     $line->append($token);
    161▕                 } else {
  ➜ 162▕                     throw new TransportException("connection to server '" . $this->config["host"] . ":" . $this->config["port"] . "' lost");
    163▕                 }
    164▕             } else {
    165▕                 $line->append($data);
    166▕             }

      +1 vendor frames 
  2   app/Console/Commands/TeamspeakBot.php:113
      PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery::wait()

  3   app/Console/Commands/TeamspeakBot.php:150
      App\Console\Commands\TeamspeakBot::start_bot(Object(App\Models\Instance))

The client connects as expected, but after exactly 300 seconds, the timeout kicks in and the clients looses the connection.

Can anyone see an issue in my code or is it an issue of this project? I ran out of ideas. :(

Sebbo94BY commented 1 year ago

When I change in the while (true) endless loop to the emit() instead of the wait() function, everything works as expected:

<?php

namespace App\Console\Commands;

use App\Models\Instance;
use Illuminate\Console\Command;
use PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException;
use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException;
use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Signal;
use PlanetTeamSpeak\TeamSpeak3Framework\TeamSpeak3;

class StartBot extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'bot:start';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Starts a TeamSpeak bot.';

    /**
     * Callback method for 'serverqueryWaitTimeout' signals.
     *
     * @param integer $seconds
     * @return void
     */
    public static function onWaitTimeout(int $idle_seconds, ServerQuery $serverquery)
    {
        // This is mostly for debug, you do not need. Remove if you do not want extra messages filling up screen.
        // Every 20 seconds, print 'seconds since last event or msg from server'
        if ($idle_seconds % 10 == 0) {
            echo "No reply from the server for $idle_seconds seconds\n";
        }

        // If the timestamp on the last query is more than 300 seconds (5 minutes) in the past, send 'keepalive'
        // 'keepalive' command is just server query command 'clientupdate' which does nothing without properties. So nothing is changes.
        if($serverquery->getQueryLastTimestamp() < time()-250)
        {
            echo "Sending keep-alive\n";
            $serverquery->request("clientupdate");
        }
    }

    /**
     * Starts the bot.
     */
    public function start_bot(Instance $instance)
    {
        $this->info("Starting TeamSpeak bot instance: ".$instance->voice_port);

        $TS3PHPFramework = new TeamSpeak3();

        $connection_uri = "serverquery://$instance->serverquery_username:$instance->serverquery_password@$instance->host:$instance->serverquery_port/?server_port=$instance->voice_port&nickname=$instance->client_nickname&ssh=1&blocking=0";

        try {
            $virtualserver = $TS3PHPFramework->factory($connection_uri);
        } catch (TransportException $e) {
            throw new TransportException($e->getMessage(), $e->getCode());
        }

        if (! is_null($instance->default_channel_id)) {
            try {
                $virtualserver->clientMove($virtualserver->whoamiGet("client_id"), $instance->default_channel_id);
            } catch (ServerQueryException $e) {
                throw new ServerQueryException($e->getMessage(), $e->getCode());
            }
        }

        // register a callback for serverqueryWaitTimeout events
        Signal::getInstance()->subscribe("serverqueryWaitTimeout", [__CLASS__, 'onWaitTimeout']);

        // wait for events
        while (true) {
            Signal::getInstance()->emit("serverqueryWaitTimeout", [time()-$virtualserver->getAdapter()->getQueryLastTimestamp(), $virtualserver->getAdapter()]);
            sleep(5);
        }
    }

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $instance = Instance::all()->first();

        try {
            $this->start_bot($instance);
        } catch (TransportException $transport_exception) {
            $this->error($transport_exception->getMessage());
        }
    }
}

The callback is called, the keep-alive sent and the client stays connected:

www-data@e33647dcf607:~$ php artisan bot:start
Starting TeamSpeak bot instance: 9987
No reply from the server for 0 seconds
No reply from the server for 10 seconds
No reply from the server for 20 seconds
No reply from the server for 30 seconds
No reply from the server for 40 seconds
No reply from the server for 50 seconds
No reply from the server for 60 seconds
No reply from the server for 70 seconds
No reply from the server for 80 seconds
No reply from the server for 90 seconds
No reply from the server for 100 seconds
No reply from the server for 110 seconds
No reply from the server for 120 seconds
No reply from the server for 130 seconds
No reply from the server for 140 seconds
No reply from the server for 150 seconds
No reply from the server for 160 seconds
No reply from the server for 170 seconds
No reply from the server for 180 seconds
No reply from the server for 190 seconds
No reply from the server for 200 seconds
No reply from the server for 210 seconds
No reply from the server for 220 seconds
No reply from the server for 230 seconds
No reply from the server for 240 seconds
No reply from the server for 250 seconds
Sending keep-alive
No reply from the server for 10 seconds
No reply from the server for 20 seconds
No reply from the server for 30 seconds
No reply from the server for 40 seconds
No reply from the server for 50 seconds
No reply from the server for 60 seconds
No reply from the server for 70 seconds
No reply from the server for 80 seconds
No reply from the server for 90 seconds
No reply from the server for 100 seconds
No reply from the server for 110 seconds
No reply from the server for 120 seconds
No reply from the server for 130 seconds
No reply from the server for 140 seconds
No reply from the server for 150 seconds
No reply from the server for 160 seconds
No reply from the server for 170 seconds
No reply from the server for 180 seconds
No reply from the server for 190 seconds
No reply from the server for 200 seconds
No reply from the server for 210 seconds
No reply from the server for 220 seconds
No reply from the server for 230 seconds
No reply from the server for 240 seconds
No reply from the server for 250 seconds
Sending keep-alive
No reply from the server for 10 seconds
...

When I var_dump() at vendor\planetteamspeak\ts3-php-framework\src\Adapter\ServerQuery.php:L165 within the wait() function before the do-while loop this: var_dump($this->getTransport());, it returns the following for me:

object(PlanetTeamSpeak\TeamSpeak3Framework\Transport\TCP)#1599 (4) {
  ["config":protected]=>
  array(8) {
    ["host"]=>
    object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1596 (2) {
      ["string":protected]=>
      string(14) "localhost"
      ["position":protected]=>
      int(0)
    }
    ["port"]=>
    int(10022)
    ["timeout"]=>
    int(10)
    ["blocking"]=>
    int(0)
    ["tls"]=>
    int(0)
    ["ssh"]=>
    int(1)
    ["username"]=>
    object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1595 (2) {
      ["string":protected]=>
      string(11) "serveradmin"
      ["position":protected]=>
      int(0)
    }
    ["password"]=>
    object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1597 (2) {
      ["string":protected]=>
      string(22) "censoredPassword"
      ["position":protected]=>
      int(0)
    }
  }
  ["stream":protected]=>
  resource(653) of type (stream)
  ["session":protected]=>
  resource(652) of type (SSH2 Session)
  ["adapter":protected]=>
  object(PlanetTeamSpeak\TeamSpeak3Framework\Adapter\ServerQuery)#1598 (6) {
    ["options":protected]=>
    array(8) {
      ["host"]=>
      object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1596 (2) {
        ["string":protected]=>
        string(14) "localhost"
        ["position":protected]=>
        int(0)
      }
      ["port"]=>
      int(10022)
      ["timeout"]=>
      int(10)
      ["blocking"]=>
      int(0)
      ["tls"]=>
      int(0)
      ["ssh"]=>
      int(1)
      ["username"]=>
      object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1595 (2) {
        ["string":protected]=>
        string(11) "serveradmin"
        ["position":protected]=>
        int(0)
      }
      ["password"]=>
      object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1597 (2) {
        ["string":protected]=>
        string(22) "censoredPassword"
        ["position":protected]=>
        int(0)
      }
    }
    ["transport":protected]=>
    *RECURSION*
    ["host":protected]=>
    object(PlanetTeamSpeak\TeamSpeak3Framework\Node\Host)#1601 (16) {
      ["parent":protected]=>
      *RECURSION*
      ["server":protected]=>
      NULL
      ["nodeId":protected]=>
      int(0)
      ["nodeList":protected]=>
      NULL
      ["nodeInfo":protected]=>
      array(0) {
      }
      ["storage":protected]=>
      array(4) {
        ["_login_user"]=>
        string(11) "serveradmin"
        ["_login_pass"]=>
        string(32) "otherCensoredPassword"
        ["_query_nick"]=>
        string(6) "MyNickname"
        ["_server_use"]=>
        array(2) {
          [0]=>
          string(18) "serverSelectByPort"
          [1]=>
          array(1) {
            [0]=>
            int(9987)
          }
        }
      }
      ["whoami":protected]=>
      array(11) {
        ["virtualserver_status"]=>
        object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1626 (2) {
          ["string":protected]=>
          string(6) "online"
          ["position":protected]=>
          int(0)
        }
        ["virtualserver_id"]=>
        int(6)
        ["virtualserver_unique_identifier"]=>
        object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1629 (2) {
          ["string":protected]=>
          string(28) "81vRlqHnHctHQMj/EKnztIgn/fU="
          ["position":protected]=>
          int(0)
        }
        ["virtualserver_port"]=>
        int(9987)
        ["client_id"]=>
        int(804)
        ["client_channel_id"]=>
        int(147)
        ["client_nickname"]=>
        object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1616 (2) {
          ["string":protected]=>
          string(6) "MyNickname"
          ["position":protected]=>
          int(0)
        }
        ["client_database_id"]=>
        int(1)
        ["client_login_name"]=>
        object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1631 (2) {
          ["string":protected]=>
          string(11) "serveradmin"
          ["position":protected]=>
          int(0)
        }
        ["client_unique_identifier"]=>
        object(PlanetTeamSpeak\TeamSpeak3Framework\Helper\StringHelper)#1628 (2) {
          ["string":protected]=>
          string(11) "serveradmin"
          ["position":protected]=>
          int(0)
        }
        ["client_origin_server_id"]=>
        int(0)
      }
      ["version":protected]=>
      NULL
      ["serverList":protected]=>
      NULL
      ["permissionEnds":protected]=>
      NULL
      ["permissionList":protected]=>
      NULL
      ["permissionCats":protected]=>
      NULL
      ["predefined_query_name":protected]=>
      string(6) "MyNickname"
      ["exclude_query_clients":protected]=>
      bool(false)
      ["start_offline_virtual":protected]=>
      bool(false)
      ["sort_clients_channels":protected]=>
      bool(false)
    }
    ["timer":protected]=>
    int(1678151488)
    ["count":protected]=>
    int(4)
    ["block":protected]=>
    array(1) {
      [0]=>
      string(4) "help"
    }
  }
}

And at the same line, var_dump($this->getTransport()->readLine()); returns absolutely nothing.

...thus var_dump($evt->section(TeamSpeak3::SEPARATOR_CELL)->startsWith(TeamSpeak3::EVENT)); in the wait() function returns Undefined variable $evt.

There were one change regarding PHP 8 support in this function:

- } while ($evt instanceof StringHelper && !$evt->section(TeamSpeak3::SEPARATOR_CELL)->startsWith(TeamSpeak3::EVENT));
+ } while (!$evt->section(TeamSpeak3::SEPARATOR_CELL)->startsWith(TeamSpeak3::EVENT));

But besides this, I didn't found any other relevant changes, which could cause this behaviour. However, changing this line back also doesn't fix it.

@ronindesign Can you reproduce this issue? Is it a problem of this project or my code? 😅

Sebbo94BY commented 1 year ago

Yes, there is actually a bug in the code of this project. I've fixed it and also added some PHPUnit tests for the future.: https://github.com/planetteamspeak/ts3phpframework/pull/190 :)