Skip to content
This repository has been archived by the owner on Oct 14, 2020. It is now read-only.

the "socket closed" and other issues - with free demo content #59

Open
MINORITYmaN opened this issue Sep 27, 2018 · 10 comments
Open

the "socket closed" and other issues - with free demo content #59

MINORITYmaN opened this issue Sep 27, 2018 · 10 comments
Assignees

Comments

@MINORITYmaN
Copy link

MINORITYmaN commented Sep 27, 2018

hi there,
some long time ago #33 had some suggestions and there was even a discussion #3
lets get it on
in case class is used for a bot in daemon mode (runs infinite in the backbround doing his assigned tasks)
did you know, that this line if(empty($data))
d9f7d46#diff-6d8bfb1cf539d16b81e45de812c980cbR4285
will never get evaluated if the socket dies (server restart, server stop, crash)?
the loop will stuck in a endless fgets, cpu core will go 100%, i metioned this issue allrady here #33
but it got ignored, and surprisingly nothing changed since then.
you cant read any data if there is no resource socket, its a blackhole endless loop.
if you dont belive, try it out, use this and turn the server off after 10 sec, and watch your bots/scripts CPU load and watch your console.

<?PHP
$ts3_ip = '127.0.0.1';
$ts3_queryport = 10011;
$ts3_user = 'serveradmin';
$ts3_pass = 'password';
$ts3_port = 9987;
require("../lib/ts3admin.class.php");
$tsAdmin = new ts3admin($ts3_ip, $ts3_queryport);
if($tsAdmin->getElement('success', $tsAdmin->connect())) {
	$tsAdmin->login($ts3_user, $ts3_pass);
	$tsAdmin->selectServer($ts3_port);
	while(1){
		print_r($tsAdmin->version());
		sleep(2);
	}
}else{
	echo 'Connection could not be established.';
}
?>

on server "crash" we should see in the console
c
but as you see, we see nothing, only our CPU goes crazy.

my solution below:

		do {
			if(is_resource( $this->runtime['socket'] ) === false || @feof( $this->runtime['socket'] ) === true){
				if(is_resource( $this->runtime['socket'] ) === true){
					@fclose($this->runtime['socket']);
				}
				$this->runtime['socket'] = $this->runtime['bot_clid'] = '';
				$this->addDebugLog('Socket closed.', $tracert[1]['function'], $tracert[0]['line']);
				return $this->generateOutput(false, array('Socket closed.'), false);
			}
			
			$data .= @fgets($this->runtime['socket'], 4096);
			
			if(strpos($data, 'error id=3329 msg=connection') !== false) {
				$this->runtime['socket'] = $this->runtime['bot_clid'] = '';
				$this->addDebugLog('You got banned from server. Socket closed.', $tracert[1]['function'], $tracert[0]['line']);
				return $this->generateOutput(false, array('You got banned from server. Connection closed.'), false);
			}
			
		} while(strpos($data, 'msg=') === false or strpos($data, 'error id=') === false);

check if the socket is still alive and not at its end of "life" before we use fgets, so you can safely fetch data, and i repeat again if the socket dies we should get this output:
c
honestly, we get nothing but a blackhole loop, no output, no errors and a 100% cpu load.

so i hope you can eval this issue to see that i dont talk rubbish.

kind regards, stefano

edit: this post's code can be ignored because i came up with new updates, see the posts below.

@MINORITYmaN
Copy link
Author

MINORITYmaN commented Sep 27, 2018

i came up with a few new ideas and tested those.
results:
refactored isConnected()

  • check if socket is empty OR if we can put data into socket ~ has small footprint
  • a resource always remains till next commands get send which fail - so we check it before this happens
  • we nicely close the socket as it should be done.
    removed private because scripts/bots should have the ability to check if hes still connected before he spams a lot a commands. in my cases i use it a lot.
	function isConnected() {
		if( !$this->runtime['socket'] || !@fputs($this->runtime['socket'], "\n") ){
			if( is_resource( $this->runtime['socket'] ) ){
				$this->runtime['socket'] = $this->runtime['bot_clid'] = '';
				@fclose( $this->runtime['socket'] );
			}
			return false;
		}
		return true;
	}

refactored executeCommand()

  • the additional checks are not more needed, i tested it on $ts->channelList('-topic -flags -voice -limits -icon -secondsempty') with 20.000 channels, worked like charm and as expected when the server was killed (crash), rebooted, stopped and when the socket got closed by firewall. debug has his error queued and cpu was totaly normal.
		do {
			$data .= @fgets($this->runtime['socket'], 4096);
			
			if(strpos($data, 'error id=3329 msg=connection') !== false) {
				$this->runtime['socket'] = $this->runtime['bot_clid'] = '';
				$this->addDebugLog('You got banned from server. Socket closed.', $tracert[1]['function'], $tracert[0]['line']);
				return $this->generateOutput(false, array('You got banned from server. Connection closed.'), false);
			}
			
		} while(strpos($data, 'msg=') === false or strpos($data, 'error id=') === false);

i would suggest you test it in your env's and perhaps it will fit your expectations too to get in a future PR, if you dont find the time, let me know, i will prepare a current PR based on this research.

some other stuff missing imho:
new accessible method, when transfering debug lines into an external file, we should clear the runtime debug log somehow.

/**
  * clearDebugLog
  * 
  * Clears the debug log
  *
  * @author     Stefan Zehnpfennig
  * @return     empty array debugLog
  */
 	public function clearDebugLog() {
		return $this->runtime['debug']=[];
	}

modified the debug output to contain a timestamp, when something happens, we should know when it happend.

/**
  * addDebugLog
  * 
  * Adds an entry to debugLog
  *
  * @author     Stefan Zehnpfennig
  * @param	string	$text			text which should added to debugLog
  * @param	string	$methodName		name of the executed method [optional]
  * @param	string	$line			line where error triggered [optional]
  * @return     void
  */
	private function addDebugLog($text, $methodName = '', $line = '') {
		if(empty($methodName) and empty($line)) {
			$backtrace = debug_backtrace();
			$methodName = $backtrace[1]['function'];
			$line = $backtrace[0]['line'];
		}
		$this->runtime['debug'][] = date( 'Y-m-d H:i:s' ) . ' Error in '.$methodName.'() on line '.$line.': '.$text;	
	}
}

ahhh and last not least:
modified, reduced the ammount of code, this worked like charm in my env for years.

	private function getData($mode, $command) {
	
		$validModes = array('boolean', 'array', 'multi', 'plain');
	
		if(!in_array($mode, $validModes)) {
			$this->addDebugLog($mode.' is an invalid mode');
			return $this->generateOutput(false, array('Error: '.$mode.' is an invalid mode'), false);
		}
		
		if(empty($command)) {
			$this->addDebugLog('you have to enter a command');
			return $this->generateOutput(false, array('Error: you have to enter a command'), false);
		}
		
		$fetchData = $this->executeCommand($command, debug_backtrace());
		
		$fetchData['data'] = str_replace(array('error id=0 msg=ok', chr('01')), '', $fetchData['data']);
		
		
		if($fetchData['success']) {
			if($mode == 'boolean') {
				return $this->generateOutput(true, array(), true);
			}
			
			if($mode == 'array' || $mode == 'multi'){
				if(empty($fetchData['data'])) { return $this->generateOutput(true, array(), array()); }
				$datasets = explode('|', $fetchData['data']);

				$output = array();

				foreach($datasets as $datablock) {
					$datablock = explode(' ', $datablock);

					$tmpArray = array();

					foreach($datablock as $dataset) {
						$dataset = explode('=', $dataset, 2);
						$tmpArray[ $this->unEscapeText($dataset[ 0 ]) ] = isset( $dataset[ 1 ] ) ? $this->unEscapeText( $dataset[ 1 ] ) : '';				
					}
					
					if($mode == 'array'){
						return $this->generateOutput(true, array(), $tmpArray);
					}

					$output[] = $tmpArray;
				}
				return $this->generateOutput(true, array(), $output);
			}
			
			if($mode == 'plain') {
				return $fetchData;
			}
		}else{
			return $this->generateOutput(false, $fetchData['errors'], false);
		}
	}

@MINORITYmaN
Copy link
Author

MINORITYmaN commented Sep 28, 2018

as i expected, the readChatMessage() needed some care, now the method works like charm.

  • the $tracert generated notices cuz it wasnt referenced,
  • refactored a bit the part at if(isConnected()) with proper debug message, closing and return, cuz we should not continue the method if the socket is down.
/**
  * readChatMessage
  * 
  * Read chat message by its type (Result: http://bit.ly/2dtBXnT)
  *
  * IMPORTANT: Check always for message success, sometimes you can get an empty message 
  * and it will return empty data
  * 
  * <b>Output:</b>
  * <pre>
  * Array
  * {
  *		[invokerid] => 37
  *		[invokeruid] => /jl8QCHJWrHDKXgVtF+9FX7zg1E=
  *		[invokername] => toxiicdev.net
  *		[msg] => It's just a prank bro
  *		[targetmode] => 3
  * }
  * </pre>
  * @author	toxiicdev 
  * @param	string	$type		textserver|textchannel|textprivate
  * @param	boolean	$keepalive	default false
  * @param	int	$cid		channel id (required only for textchannel)
  * @return	array	data
  */	
	public function readChatMessage($type = 'textchannel', $keepalive = false, $cid = -1)
	{
		$tracert = debug_backtrace();
		$availTypes = array('textserver', 'textchannel', 'textprivate');
		$rtnData = array('success' => 0, 'data' => array('invokerid' => '', 'invokeruid' => '', 'invokername' => '', 'msg' => '', 'targetmode' => ''));
		
		if(!$this->isConnected()) {
			$this->runtime['socket'] = $this->runtime['bot_clid'] = '';
			$this->addDebugLog('script isn\'t connected to server', $tracert[1]['function'], $tracert[0]['line']);
			return $this->generateOutput(false, array('script isn\'t connected to server'), false);
		}
		
		if(!in_array($type, $availTypes)) {
			$this->addDebugLog('Invalid passed read type', $tracert[1]['function'], $tracert[0]['line']);
			return $rtnData;
		}
		
		if(!$this->runtime['selected']) { return $this->checkSelected(); }
		
		$whoami = $this->whoami();
		
		if(!$whoami['success']) { return $whoami; }
		
		if($type == 'textchannel' && $whoami['data']['client_channel_id'] != $cid)
		{
			$this->clientMove($this->getQueryClid(), $cid);
		}
		
		$this->executeCommand("servernotifyregister event=$type" . ($cid != -1 ? " id=$cid" : "") , null);
		
		$data = fgets($this->runtime['socket'], 4096);
		
		if(!empty($data))
		{		
			$rtnData['success'] = 1;
			$msgData = explode(" ", $data);
			foreach($msgData as $param)
			{
				$paramData = explode("=", $param);
				if(array_key_exists($paramData[0], $rtnData['data']))
				{
					$rtnData['data'][$paramData[0]] = $this->unescapeText(implode("=", array_slice($paramData, 1, count($paramData) -1)));
				}
			}
		}
		if(!$keepalive) $this->serverNotifyUnregister();
		
		return $rtnData;
	}

@MINORITYmaN MINORITYmaN changed the title the socket closed issue - with free demo content the "socket closed" and other issues - with free demo content Sep 28, 2018
@MINORITYmaN
Copy link
Author

imho quit() should be non private for advanced bots.

@Speckmops
Copy link
Owner

Hey @MINORITYmaN thanks for your work! could you create a new pull request with all these changes?

@Speckmops Speckmops self-assigned this Oct 5, 2018
@MINORITYmaN
Copy link
Author

hi @par0noid,
sure, i have to finish a customers project, then i come back to topic.
my last dev version got some new stuff, should be interesting.

@Pantoflarz
Copy link
Contributor

this seemed very interesting and quite important - any news since?

@lgund
Copy link
Collaborator

lgund commented Dec 14, 2018

Looks like @par0noid ist currently inactive. We need to wait until he will be back.

@MINORITYmaN is it possible to create also a pull request on my fork? I´ve changed a bit of the class for ssh support.

@Speckmops
Copy link
Owner

I'm sorry for my inactivity. Got a lot to do at work. I invited you @lgund to collaborate on this repo. You seem to be very active and competent.

@lgund Ich weiß nicht ob du wirklich Interesse dran hast hier ein wenig zu "administrieren" und Pull-Requests zu bestätigen (nach Prüfung natürlich), oder vielleicht auch Code zu ändern. Ich weiß nur leider aktuell nicht wo ich die Zeit her nehmen soll 😢 Hab auch leider ein bisschen den Eindruck gewonnen, dass Teamspeak 3 irgendwie nicht mehr so viel "Sinn" macht, Jeder scheint Discord zu nutzen und Teamspeak 5 ist ja auch im Gespräch. Dazu kommt noch, dass dieser Programmierstil ohne OOP von 2009 überhaupt nicht mehr meiner Natur entspricht, man entwickelt sich ja auch weiter. Fühle mich ein bisschen als ob ich das Projekt nur noch durch sporadische Updates "am Leben halte". Falls du also Interesse hast, erstelle ich gerne bei Versionswechseln eine neue Doku und aktualisiere die Homepage - für mehr hab ich leider aktuell nicht die Zeit 😭

@lgund
Copy link
Collaborator

lgund commented Dec 15, 2018

Da TeamSpeak 5 wird sich gegenuber TeamSpeak 3 beim Server nicht großartig ändern. Ich kann dieses repro gerne supporten solange du keine Zeit hast.

Mach dir keine Sorgen wegen oop. Finde den Stil bei php völlig überflüssig. Aber habe eh allgemein kein gutes Bild von php.

@foerkede
Copy link

foerkede commented Dec 2, 2019

Any update on this topic? I really need this fix :D
I could also create a pull request with the code by @MINORITYmaN if it is still working.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants