SIM Status

From Second Life Wiki
Revision as of 09:07, 25 January 2015 by ObviousAltIsObvious Resident (talk | contribs) (<lsl> tag to <source>)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Created by Kira Komarov.

Web-Related Tree

+-------------+ +-------------+ +---------+ +-----------------------------+ +--------------------------+ | Prowler | | Gremlin | | N2K | | Permanent Primitive URL | | Collaborative Coding | +------+------+ +-------------+ +---------+ +------------+----------------+ +--------------------------+ | | +--------+--------+ +-------------------+-------------------+ | SIM Status | | Population Genetics and Selection | +-----------------+ +-------------------+-------------------+ | +------------+-------------+ | Interactive Bacteria | +--------------------------+

ChangeLog

9 March 2012

7 March 2012

  • Redesigned the script and included some uptime tracking - will become part of a bigger project.

Introduction

This is a script that will display simulator statuses. The script reads a notecard called Regions containing simulator names and then scans for the simulator statues and displays them in text above the prim. There are a number of states in which a certain simulator might be in, these are:

  • "up": simulator currently up and running
  • "down": simulator currently down
  • "starting": simulator currently starting
  • "stopping": simulator currently stopping
  • "crashed": simulator has crashed
  • "unknown": simulator status unknown or unknown simulator

Connector Flow Diagram

         Region Name - [up] T1:20:30
         Region Name - [up] T1:20:30
         ...
           *                        POST req. +-------------------+
           |   +------------+     +-  - - - ->| Prowl->iOS/PC/ETC |
 Primitive |   | Primitive  |     |           +-------------------+
   Text    |   | Inventory  |
  Display  |   +------------+     |
           +---| SIM Status |
               |     *      |     |
               |     |      |
               |     *      |     |
   Optional -> |  Prowler   |- - -+
               |     |      |
   Optional -> | xHTTP Comp |   GET req.      +------------------+
               |  Regions   |<- - - - - - - ->|    PHP Server    |
               +------------+  http resp.     +------------------+  Region Lookup
                                              | simStatus.php    |<-----+
                                              |        +         |      |
                                              |        |         |      |
                                              |        +         |      |
                                              | simStatus.sqlite |<-----+
                                              +------------------+  Update URLs

Prowl Integration

Since we derive scripts, we integrate them as well. The integration with Prowler is already in the script. All you need to do is update your API key in the code to your Prowl API key and add the Prowler script to the primitive so that you receive updates when the simulator's state changes.

The inventory of the primitive will then contain:

SIM Status
xHTTP Component
Prowler
Regions

If you have not read the Prowler page, Prowl allows you to receive notifications on your mobile devices. We use that in order to send a notification to an iOS device when a simulator's state changes.

Website Table (Non-Integrated)

The non-integrated version display of the SIM statuses

The table may be integrated anywhere on an arbitrary website. The display itself will change in order to reflect the changes to the notecard. All you have to do is add or remove SIMs from the notecard in the primitive and everything else is taken care of by the scripts.

Usage

The scanning for the SIM statuses can be done from any parcel and does not require the script to be present on that parcel. This is useful for estate managers to monitor the status of their SIMs.

To use this script, create a prim and add a notecard inside it called Regions in which you list all the simulator names you wish the script to monitor on every single line. For example, a notecard containing three simulators to monitor, might look something like:

Meihano Bay
Azure Falls
Jersey Island

Please note that a blank newline is needed after the simulator entries as in the example above.

After that, drop the script below in the prim and the script will take care of all the rest. After some time, the statuses of your simulators will be displayed in white text above the prim, every SIM being updated every second.

Limitations

None other than the limits imposed by Limits for the notecard and the Guidelines for Private Region Naming

Code

The first script, SIM Status monitors regions status changes and the second script xHTTP Component answers to incoming HTTP requests by pulling data from SIM Status and formatting it to XML.

Code: SIM Status

//////////////////////////////////////////////////////////
// [K] Kira Komarov - 2012, License: GPLv3              //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////

key nQuery = NULL_KEY;
integer nLine = 0;
list nList = [];
string curSim = "";

//pragma inline
readRegions() {
    if(llGetInventoryType("Regions") == INVENTORY_NOTECARD) jump read;
    llOwnerSay("Failed to find notecard.");
    return;
@read;
    nQuery = llGetNotecardLine("Regions", 0);
}


default
{
    state_entry() {
        llSetText("", <0,0,0>, 0);
        readRegions();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llSensorRemove();
            llSetTimerEvent(0);
            llResetScript();
        }
    }
    timer() {
        llSetTimerEvent(0);
        curSim = llList2String(llParseString2List(llList2String(nList, 0), [" - "], []), 0);
        nQuery = llRequestSimulatorData(curSim, 6);
    }
    no_sensor() {
        if(llList2String(llParseString2List(llList2String(nList, 0), ["[ ", " ]"], []), 1) != nQuery) {
            nQuery = curSim + " - [ " + (string)nQuery + " ] T0:0:0";
            // Comment the following lines out between the --MARK-- if you do not want Prowler support.
            // Otherwise, make sure that you replace your PROWL_API_KEY. You can find instructions on
            // the Wizardry and Steamworks wiki @ http://was.fm/wiki/Prowler
            // --MARK--
            integer cha = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
            llMessageLinked(LINK_THIS, cha, "PROWL_API_KEY,6f5902ac237024bdd0c176cb93063dc4,PROWL_APPLICATION,SIM Status,PROWL_EVENT,SIM Status Change,PROWL_DESCRIPTION," + "SIM: " + curSim + " is now: " + (string)nQuery, "");
            // --MARK--
            jump updated;
        }
        nQuery = llList2String(llParseString2List(llList2String(nList, 0), ["T"], []), 1);
        nLine = llList2Integer(llParseString2List(nQuery, [":"], []), 2) + llList2Integer(llParseString2List(nQuery, [":"], []), 1) * 60 + llList2Integer(llParseString2List(nQuery, [":"], []), 0) * 3600 + 1;
        nQuery = llList2String(llParseString2List(llList2String(nList, 0), ["T"], []), 0) + "T" + (string)(nLine/3600) + ":" + (string)((nLine%3600)/60) + ":" + (string)((nLine%3600)%60);
@updated;
        nList = llDeleteSubList(nList, 0, 0);
        nList += nQuery;     
        llSetText(llDumpList2String(nList, "\n"), <1,1,1>, 1.0);
        llSetTimerEvent(1);
    }
    link_message(integer sender_num, integer num, string str, key id) {
        if(str == "SYN") llMessageLinked(LINK_THIS, 0, llList2CSV(nList), "ACK");
    }
    dataserver(key id, string data) {
        if(id != nQuery) return;
        if(data == EOF || data == "" || llStringLength(data) > 8) jump read;
        llSensor("", nQuery = data, AGENT, 0.1, 0);
        return;
@read;
        if(data == EOF) {
            llSetTimerEvent(1);
            return;
        }
        if(data == "") jump skip;
        nList += data;
@skip;
        nQuery = llGetNotecardLine("Regions", ++nLine);
    }
}

Code: xHTTP Component

The description of the XML structure is given by:

<xsim>
  <sim>name of the region</sim>
  <status>reported status, up, down, crashed, etc...</status>
  <valid>time since when the status is valid</valid>
</xsim>

The <valid> tag represents the time since the <status> tag has been updated. In that sense:

  • If the simulator is up, then the <valid> tag reports how much time it has been up.
  • If the simulator is down, then the <valid> tag reports how much time the simulator has been down.

etc...

The code uses jumps to concatenate and convert the data to a simple XML format:

//////////////////////////////////////////////////////////
// [K] Kira Komarov - 2012, License: GPLv3              //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////
//                   CONFIGURATION                      //
//////////////////////////////////////////////////////////

// Set this URL to point to the simStatus.php script 
// on the webserver hosting the PHP script:
string llURL = "http://was.fm/SIM/simStatus.php";

// Set this to the password in the PHP script.
// Please see the PHP script headers for documentation.
string apiKey = "bbd16d2772593ea5f890973c027dbc7f4a096164";

//////////////////////////////////////////////////////////
//                  END CONFIGURATION                   //
//////////////////////////////////////////////////////////

default
{
    state_entry() {
        llRequestURL();
        llSetTimerEvent(5);
    }
    timer() {
        llSetTimerEvent(0);
        if(llURL == "") {
            llRequestURL();
            llSetTimerEvent(5);
            return;
        }
        
    }
    http_request(key id, string method, string body) {
        if (method == URL_REQUEST_GRANTED) {
            llHTTPRequest(llURL + "?url=" + llEscapeURL(body) + "&key="+apiKey, [], ""); 
            llURL = body;
            return;
        }
    }
    http_response(key request_id, integer status, list metadata, string body) {
        if(status == 200) {
            state catch;
        }
    }
}
state catch
{
    http_request(key id, string method, string body) {
        if (method == "GET") {
            llURL = id;
            state finally;
            return;
        }
    }
    changed(integer change) {
        llResetScript();
    }
}

state finally
{
    state_entry() {
        llMessageLinked(LINK_THIS, 0, "SYN", "");
    }
    link_message(integer sender_num, integer num, string str, key id) {
        if(id != "ACK") return;
        list xData = llParseString2List(llDumpList2String(llCSV2List(str), "|"), ["|", " - ", "[ ", " ]", " ˉ|ˉ"], []);
        str = "<xsim>";
@tag;
        id = llList2String(xData, 0);
        xData = llDeleteSubList(xData, 0, 0);
        if((string)id == "up" || (string)id == "down" || (string)id == "starting" || (string)id == "stopping" || (string)id == "crashed" || (string)id == "unknown") {
            str += "<status>" + (string)id + "</status>";
            jump tag;
        }
        if(~llSubStringIndex((string)id, ":")) {
            str += "<valid>" + (string)id + "</valid>";
            jump tag;
        }
        if((string)id != "") {
            str += "<sim>" + (string)id + "</sim>"; 
            jump tag;
        }
        llHTTPResponse(llURL, 200, str+"</xsim>");
        state catch;
    }
}

Code: PHP Server-Side module

The following script, along with an sqlite database has to be placed on a webserver with PHP support. All the set-up instructions are contained within the PHP script below.

<?php

	//////////////////////////////////////////////////////////
	// [K] Kira Komarov - 2012, License: GPLv3              //
	// Please see: http://www.gnu.org/licenses/gpl.html     //
	// for legal details, rights of fair usage and          //
	// the disclaimer and warranty conditions.              //
	//////////////////////////////////////////////////////////
	
	// This script requires:
	// pecl_http, you can install pecl with:
	// sh$ pecl install pecl_http
	
	// This script relies on the following PHP support:
	// PHP simplexml (usually built in)
	// PHP JSON (usually built in)
	// PHP sqlitev3 support (usually built in)
	// PHP PDO, sqlite connector (usually built in)
	
	// This script relies on the following services:
	// Google Charts
	
	// You need to create an sqlite database on the webserver
	// which will update the URLs provided by llRequestURL().
	
	// The following represent the shell commands you would
	// need to execute in order to set-up the sqlite database.
	
	// The database has to be created in the same directory 
	// where this PHP script is placed.
	
	// Create Database:
	// sh$ sqlite3 simStatus.sqlite
	// SQLite version 3.6.12
	// Enter ".help" for instructions
	// Enter SQL statements terminated with a ";"
	// sqlite> CREATE TABLE "main"."urls" (
	//    ...> "URL" text NOT NULL,
	//    ...> PRIMARY KEY("URL")
	//    ...> );
	// sqlite> .quit
	// sh$
	
	// The folder on the webserver will contain the following:
	// simStatus.php
	// simStatus.sqlite
	
	// Enjoy. [WaS-K]
	
	// For privacy, you need to generate a password. You can do that
	// by issuing the following command on an *nix-like system and 
	// replace "super-duper secret password" with a secret shared between
	// this PHP script and the LSL counterpart:
	// sh$ echo "super-duper secret password" | sha1sum  | awk '{ print $1 }'
	$apiKey = 'bbd16d2772593ea5f890973c027dbc7f4a096164';

	if(isset($_GET['url']) && isset($_GET['key'])) {
		if($_GET['key'] != $apiKey) die;
		$llURL = $_GET['url'];
		$db = new PDO('sqlite:simStatus.sqlite');
		$q = $db->prepare("REPLACE INTO urls(URL) VALUES(:url)");
		try {
			$q->execute(array(':url' => $llURL));
		}
		catch(PDOException $e) {
			print $e->getMessage();
		}
		return 'OK';
	}
	$db = new PDO('sqlite:simStatus.sqlite');
	$q = $db->prepare("SELECT URL FROM urls");
	$q->execute();
	while($res = $q->fetchObject()) {
		$req = new HttpRequest($res->URL, HttpRequest::METH_GET);
		try {
			$req->send();
		}
		catch(HttpException $ex) {
			print $ex;
			die;
		}
		if($req->getResponseCode() != 200) {
			$d = $db->prepare("DELETE FROM urls WHERE URL=:url");
			$d->execute(array(':url' => $res->URL));
			continue;
		}
		$simStatus = json_decode(json_encode(simplexml_load_string($req->getResponseBody())),TRUE);
	}
?>

<html>
  <head>
    <script type='text/javascript' src='https://www.google.com/jsapi'></script>
    <script type='text/javascript'>
      google.load('visualization', '1', {packages:['table']});
      google.setOnLoadCallback(drawTable);
      function drawTable() {
        var data = new google.visualization.DataTable();
        data.addColumn('string', 'Name');
        data.addColumn('string', 'Status');
        data.addColumn('string', 'Valid');
<?php
	print "\t".'data.addRows('.count(current($simStatus)).');'."\n";
	$i=0; $j=0;
	foreach($simStatus as $node => $child) {
		$cellData = '';
		foreach($child as $value) {
			$cellData .= "\t".'data.setCell('.$i++.','.$j.','.'\''.$value.'\''.');'."\n";
		}
		++$j; $i=0;
		print $cellData;
	}
?>
        var table = new google.visualization.Table(document.getElementById('table_div'));
        table.draw(data, {showRowNumber: true});
      }
    </script>
  </head>

  <body>
    <div id='table_div'></div>
  </body>
</html>

Code: Prowler

May be found on the separate Prowler page.

Developer Notes

The new version is a bit crazy. Ok, it is a bit insane. I was wondering how far I can go with control flow in order to minimize global variable usage. Because of that, the program might seem complicated to understand. However, it does follow a simple mode of operation. Here are some pointers to understand how this works:

  • When a new line is read from the notecard, we check if the line is either EOF, blank or greater than 8 characters. This assumes that any region name is larger than 8 characters and is due to the fact that llRequestSimulatorData returns string literals, the largest being "stopped" with 8 characters. Thus, anything returned by dataserver which is shorter than 8 characters (not EOF and non-empty) is considered some response to llRequestSimulatorData. Anything else, is considered a notecard line.
  • The notecard is read as usual, using jumps to skip over blank lines and incrementing nLine on each line. When EOF is reached, the timer event handler is installed and control flow breaks. When the timer event is raised, the next scheduled timer is stopped using llSetTimerEvent with 0, the simulator/region name is extracted and llRequestSimulatorData is called using that name. When the dataserver event is raised, nQuery which is in fact a key is set to the response of llRequestSimulatorData - we do that because nQuery, which used to store the key for dataserver is not needed anymore, so we use it to store the response from llRequestSimulatorData.
  • When the no_sensor event is raised, we check whether nQuery, which is the output of our current simulator's status is not equal to the last status of the simulator. If it is not, for example if the simulator was up and now it is down, then it is clear that the simulator state has changed, so we update nList and set the time to 0:0:0 indicating that the simulator state has changed. However, if the state is still the same (for example, if the simulator was up and it is still up), then we filter out the time, convert it to seconds and add 1 second and then store it back again. This is all done on the lines:
        nQuery = llList2String(llParseString2List(llList2String(nList, 0), ["T"], []), 1);
        nLine = llList2Integer(llParseString2List(nQuery, [":"], []), 2) + llList2Integer(llParseString2List(nQuery, [":"], []), 1) * 60 + llList2Integer(llParseString2List(nQuery, [":"], []), 0) * 3600 + 1;
        nQuery = llList2String(llParseString2List(llList2String(nList, 0), ["T"], []), 0) + "T" + (string)(nLine/3600) + ":" + (string)((nLine%3600)/60) + ":" + (string)((nLine%3600)%60);

which additionally spares us the need to declare the hours, minutes and seconds as globals.

  • The nList list stores the name of the simulators and the list is used as a queue by dequeueing the first element off the stack, processing it in no_sensor, depending on any simulator state changes we alter it and then finally enqueue it back to nList.
  • I also make thorough use of jump which has the nice consequence (when used correctly and implemented correctly by the compiler) that the cyclomatic complexity decreases drastically. By using jump (which I do for most of my projects) I manage to reduce the nesting level which should majorly boost runtime performance.
  • Another observation is that timer as well as no_sensor are used non-conventionally by locking them down. That is timer becomes some alarm as per Wizardry and Steamworks and no_sensor is similarly used as an alarm - they are not used for what one would trivially use them for: instead they are used as part of the control flow in order to schedule the scan and status of the next region in nList. As you can observe, there is no global iterator that runs through the list of regions.

The main point is that due to the loosely typed key primitive, the string and key primitives are interchangeable, which allows us to elegantly carry both datatypes using one variable. The code uses (abuses) nQuery which sometimes behaves as a key-carrier for dataserver and sometimes as a string carrier for the llRequestSimulatorData response.

One advantage is that we really use a key, integer, string and a list which contains just the simulator names. This comes at the price of executing cycles (when we recompute the new updated uptime) but with the advantage of constantly-less memory consumption. The program does not declare any new variables as well during execution and the only allocation cycles are performed implicitly by things like the list processing functions (which, given a sufficiently well-written compiler would optimize and cache those).

It is doubtful (ie: for sure not!) that the script will work on the OpenSIM platform. However, there are better ways to monitor so a script like this should be redundant.