I have been using the PHP API and found an interesting problem. When the read() function is called and the response is greater than 127 characters, it all goes horribly wrong.
This is because the original coder failed to take into account the reply length criteria as defined on http://wiki.mikrotik.com/wiki/API. It took me an hour or so to get my head around how the length works and therefore how to fix the PHP code. Unfortunately, my account doesn’t have the ability to edit the API page on the wiki and so I can’t update the page to make the description more clear. However…
I have changed the read() function in the PHP class as follows:
function read($parse = true) {
$RESPONSE = array();
while (true) {
// Read the first byte of input which gives us some or all of the length
// of the remaining reply.
$BYTE = ord(fread($this->socket, 1) );
$LENGTH = 0;
echo "$BYTE\n";
// If the first bit is set then we need to remove the first four bits, shift left 8
// and then read another byte in.
// We repeat this for the second and third bits.
// If the fourth bit is set, we need to remove anything left in the first byte
// and then read in yet another byte.
if ($BYTE & 128) {
if (($BYTE & 192) == 128) {
$LENGTH = (($BYTE & 63) << 8 ) + ord(fread($this->socket, 1)) ;
} else {
if (($BYTE & 224) == 192) {
$LENGTH = (($BYTE & 31) << 8 ) + ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
} else {
if (($BYTE & 240) == 224) {
$LENGTH = (($BYTE & 15) << 8 ) + ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
} else {
$LENGTH = ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
$LENGTH = ($LENGTH << 8 ) + ord(fread($this->socket, 1)) ;
}
}
}
} else {
$LENGTH = $BYTE;
}
// If we have got more characters to read, read them in.
if ($LENGTH > 0) {
$_ = "";
$retlen=0;
while ($retlen < $LENGTH) {
$toread = $LENGTH - $retlen ;
$_ .= fread($this->socket, $toread);
$retlen = strlen($_);
}
$RESPONSE[] = $_ ;
$this->debug('>>> [' . $retlen . '/' . $LENGTH . ' bytes read.');
}
// If we get a !done, make a note of it.
if ($_ == "!done")
$receiveddone=true;
$STATUS = socket_get_status($this->socket);
if ($LENGTH > 0)
$this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . '] ' . $_);
if ( (!$this->connected && !$STATUS['unread_bytes']) ||
($this->connected && !$STATUS['unread_bytes'] && $receiveddone) )
break;
}
if ($parse)
$RESPONSE = $this->parse_response($RESPONSE);
return $RESPONSE;
}
Note that this incorporates the changes I suggested in a previous post (regarding the ‘!done’ reply). Please can somebody with better PHP skills than me have a look and see if it can be optimised? If there’s no response within a few days, I’ll make the changes on the wiki.
Hi,
I can confirm that your implementation of read method runs very nice. The original wiki code was buggy, some commands does not executes at all.
I used moly’s fix http://forum.mikrotik.com/t/php-api-bug/25484/4 But it locks for few seconds on every command. With this implementation is all fine. I would only inicialize $receiveddone = false at the beggining of method call to get rid of php notice.
Thank you!
You are quite right - as it exists at the moment, the class expects an immediate response to a command and doesn’t work at all well when the response may never come or may come at some point in the future.
well, listen commands have !done, when you send in cancel, also, listen (and all commands without the end) should have their tag set =tag=unendingStory
so you can use /cancel with =tag=unendingStory to stop the command
Ok Janis, but what is the point in listening and cancelling ? If you want to react just after event occurs than you need to modify this API to be able to read without !done message.
Yes, you are right about using tags, but to use tags, this PHP API is absolutly useless.
BTW why is not possible to use command “/export” or eg. “/system/export” or any other export via API? The behavior of script is quite weird, sometimes ends immediately, sometimes returns zeroes, sometimes waits and returns nothing. In any case without useful response. After this query routerboard is often loaded at 100% (probably permanently) and you have to restart it, otherwise system become unstable and very slow. I want to get automatically some actual configurations from my RBs using API, but without any success.
you are able to listen and while listening you are receiving data. as it is endless command it can only be canceled. Once canceled, it will return !done
but all the data will be received previously.
this is basics how API works:
API client --->command ---> router
APi client <---data !done<---router
with endless commands
API client --->command listen ---> router
APi client <---data <---router
APi client <---data <---router
..
APi client <---data <---router
APi client <---data <---router
API client ---> /cancel ----------->router
APi client <---data !done<--- router
so you are able to run several commands simultaneously. it is just a matter of implementation, when you are able to view data. So that is good idea to tag commands, because each ret will have its tag assigned, so you can see what command is returning what.
I use this for reading the length. I derived it from the python example on the API wiki.
The $this->read(X) call goes to a method that reads X bytes off the socket buffer.
private function readLength()
{
/* Get the value of the first byte */
$lengthDecoded = ord($this->read(1));
/* Check if we are a single byte */
if (((int)$lengthDecoded & 0x80) == 0x00)
{
/* Single byte length */
return $lengthDecoded;
}
/* Check if we are a 2 byte length */
else if (((int)$lengthDecoded & 0xC0) == 0x80)
{
/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
$lengthDecoded = $lengthDecoded & (~0xC0);
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
return $lengthDecoded;
}
/* Check if we are a 3 byte length */
else if (((int)$lengthDecoded & 0xE0) == 0xC0)
{
/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
$lengthDecoded = $lengthDecoded & (~0xE0);
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
return $lengthDecoded;
}
/* Check if we are a 4 byte length */
else if (((int)$lengthDecoded & 0xF0) == 0xE0)
{
/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
$lengthDecoded = $lengthDecoded & (~0xF0);
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
/* Shift the value 8 bytes */
$lengthDecoded = (int)$lengthDecoded << 8;
/* Add the next value onto the working value */
$lengthDecoded += ord($this->read(1));
return $lengthDecoded;
}
else
{
throw new RosWordTooLongException("First byte of length was: 0x".dechex($lengthDecoded));
}
}
NAB, thank you for posting this script. I had to make one modification to it as I was getting the following error:
PHP Notice: Undefined variable: receiveddone in routeros_api.class.php on line 322
I had to add an else statement so that the var was valid:
// If we get a !done, make a note of it.
if ($_ == "!done")
$receiveddone=true;
else {
$receiveddone=false;
}
I’m using this script to get the equivalent of /ip hotspot active print count-only as I can not get my old 2.9 expect scripts to work quickly enough or properly with 3.0 due to all of the garbage you get when ssh’ing into a router (yes I’ve tried +ct and 80w).
also, for outputs and testing command i would suggest to use python example from wiki page, that way you see if command you are trying to run is capable to run at all. If it is, you should be able to get same or similar output from every other API implementation. Just that python example is made my MikroTik