View Single Post
  #1   (View Single Post)  
Old 27th June 2010
Carpetsmoker's Avatar
Carpetsmoker Carpetsmoker is offline
Real Name: Martin
Tcpdump Spy
 
Join Date: Apr 2008
Location: Netherlands
Posts: 2,243
Default Online Unreal Tournament server browser with pcntl_fork()

Another Unreal Tournament related script that's I’ve been using for a long time.

Today I refactored the code to use pcntl_fork() in order to speed the script up.
If you want to any form of multiprocessing in PHP, you have little choice but to use pcntl_fork(), this function call and almost all other pcntl* and posix* calls are essentially the same as the C calls—PHP is such a “high-level” language…

Do not use exit() to exit the child process, use posix_kill(getmypid, SIGKILL) instead. For reasons not clear to me exit() doesn't work when run from CGI, it exits the parent process too (It does work when run from the commandline).

In action
Here are working demo’s:
ut-serverbrowse with fork()
Without fork()

The code with fork
Code:
  <?php
  #
  # Martin Tournoij <martin@arp242.net>
  # Free for any use. There are no restrictions.
  #
  # Note: We use pcntl_fork(), you will need the pcntl PHP module.
  # Also note this will not work in Windows or if PHP is loaded as Apache module.
  #
  
  function Query($server, $cmd)
  {
  	# http://wiki.beyondunreal.com/Legacy:UT_Server_Query
  
  	$address = gethostbyname($server['0']);
  	
  	if (!$s = fsockopen('udp://' . $server['0'], $server['1'], $errno, $errstr, 2))
  		return False;
  
  	stream_set_timeout($s, 2);
  	fputs($s, "\\$cmd\\");
  	$info = fread($s, 8192);
  	fclose($s);
  	$info2 = explode('\\', "\\$info\\");
  
  	foreach ($info2 as $k => $v)
  	{
  		if (empty($v) || $v == '' || $v == ' ')
  			continue;
  		if (is_int($k/2))
  			$pv = $v;
  		else
  			$prettyinfo["$pv"] = htmlentities($v);
  
  	}
  	$prettyinfo['str'] = $info;
  
  	return $prettyinfo;
  }
  
  function Server($s)
  {
  	$info = Query($s, 'status');
  
  	if (empty($info['str']))
  	{
  		# Just hide the entry, servers seem to be going up & down more often than 
  		# the space shuttle ...
  		#return "<tr><td colspan=\"4\">Could not connect to {$s['0']} {$s['1']}</td></tr>";
  		return '';
  	}
  	
  	if (isset($info['numplayers']))
  	{
  		$numplayers = $info['numplayers'] . " Players:<br />\n";
  		$players = Query($s, 'players');
  
  		for ($i=0; $i<=$info['maxplayers']; $i++)
  		{
  			$k = "player_$i";
  			if ($players["$k"])
  				$numplayers .= $players["$k"] . "<br />\n";
  		}
  	}
  	else
  		$numplayers = 'No players';
  
  	# XXX Detect DTAS in a better way
  	if ($info['gametype'] == 'INFCoopUnrealGame')
  		$gametype = 'Coop';
  	elseif ($info['gametype'] == 'INFg_EASGame')
  		$gametype = 'EAS';
  	elseif (stristr($info['hostname'], 'dtas'))
  		$gametype = 'DTAS';
  	elseif ($info['gametype'] == 'InfilTeamGamePlus')
  		$gametype = 'TDM';
  	else
  		$gametype = $info['gametype'];
  
  	$row = sprintf('<tr>
  		<td><a href="ut-serverbrowse.php?server=%s:%s">%s</a><br />
  		<span class="small">%s:%s</span></td>
  		<td>%s</td>
  		<td>%s</td>
  		<td>%s</td>
  		</tr>',
  		$s['0'], $s['1'], $info['hostname'], $s['0'], $s['1'], $info['mapname'], $gametype, $numplayers);
  
  	return $row;
  }
  
  function html($tbl)
  {
  	if (stristr($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml'))
  		$charset = 'application/xhtml+xml; charset=utf-8';
  	else
  		$charset = 'text/html; charset=utf-8';
  	header("Content-Type: $charset");
  
  	print '<?xml version="1.0" encoding="UTF-8"?>';
  	printf('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  			"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
  	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  		<head>
  			<meta http-equiv="Content-Type" content="%s" /> 
  			<title>Infiltration Servers</title>
  			<style type="text/css">
  				table { border-collapse: collapse; margin: .25em; width: 95%%; }
  				td, th, table, caption { border: 1px solid black; }
  				tfoot td { border: none; font-size: .8em; }
  				caption, th, tfoot { background-color: #ddd; }
  				caption { font-weight: bold; }
  				tr:hover { background-color: #eee; }
  				div { border: 1px solid black; margin: .25em; width: 94%%; padding: .2em; margin-top: 2em; }
  				label { width: 6em; }
  				.small { font-size: .75em; margin-left: 2em; }
  				p { width: 40em; }
  			</style>
  		</head>
  		<body>
  			<p>Refresh your browser and the list will be refreshed. Click on a link to 
  				refresh just that one server (faster).<br />
  				Unfortunately, the list of players is often incomplete. This is not a 
  				bug on my side but one in the UT server. Sometimes trying for a few times can get you a 
  				larger list, but often not :(</p>
  
  			<table summary="Infiltration servers">
  				<caption>Infiltration servers</caption>
  				<thead>
  					<tr>
  						<th scope="col">Server</th>
  						<th scope="col">Map</th>
  						<th scope="col">GameType</th>
  						<th scope="col">Players</th>
  					</tr>
  				</thead>
  				<tfoot>
  					<tr>
  						<td colspan="4">List fetched on %s
  							| <a href="ut-serverbrowse-fork.php.txt">View source</a></td>
  					</tr>
  				</tfoot>
  				<tbody>
  					%s
  				</tbody>
  			</table>
  
  			<form method="post" action="%s">
  				<div>
  					<label for="server">Manually enter server to query. <em>address:port</em>: </label>
  					<input type="text" id="server" name="server" value="%s"/>
  					<input type="submit" value="Query" />
  				</div>
  			</form>', $charset, date('r'), $tbl, $_SERVER['PHP_SELF'], $_GET['server']);
  
  	if (isset($_GET['server']))
  		printf('<p><a href="%s">Back to main</a></p>', $_SERVER['PHP_SELF']);
  
  	print '</body></html>';
  }
  
  # We do not use a masterserver but maintain the list here manually; This is a 
  # feature and not a bug because the masterserve for the mod I play 
  # (Infiltration) is not particuarly reliable.
  $servers = array(
  	array('cerberon.net', 17778), # coop1
  	array('cerberon.net', 27778), # coop2
  	array('90.184.183.249', 7778), # wargamez
  	array('xplod.de', '7778'), # xplod
  	array('xplod.de', '8889'), # xplod2
  	array('84.255.245.128', '7778') # planetweed
  );
  
  if (isset($_POST['server']))
  	$_GET['server'] = $_POST['server'];
  
  if (isset($_GET['server']))
  {
  	$a = explode(':', $_GET['server']);
  	$servers = array(array($a['0'], $a['1']));
  }
  
  $mypid = getmypid();
  $procs = array();
  $tmp = tempnam(sys_get_temp_dir(), 'SERVERBROWSE_');
  $fp = fopen($tmp, 'w+');
  foreach ($servers as $s)
  {
  	$pid = pcntl_fork();
  
  	if ($pid == -1)
  	{
  		print 'Unable to fork. Exiting';
  		exit(1);
  	}
  
  	if ($pid == 0)
  	{
  		$data = Server($s);
  		fwrite($fp, $data) . "\n";
  		posix_kill(getmypid(), 9);
  	}
  	else
  		array_push($procs, $pid);
  }
  
  # Wait for all childs to finish
  while (count($procs) > 0) 
  {
  	$pid = pcntl_waitpid(-1, $status, WNOHANG);
  
  	foreach($procs as $k => $p)
  	{
  		if($pid == $p)
  			unset($procs[$k]);
  	}
  	usleep(250);
  }
  
  fclose($fp);
  
  $fp = fopen($tmp, 'r');
  $tbl = '';
  while ($buf = fread($fp, 8192))
  	$tbl .= $buf;
  fclose($fp);
  unlink($tmp);
  
  html($tbl);
  exit(0);
  
  ?>
The code without fork
This is the old version without forking, it”s slower but since pcntl_fork() is not always available it may still be of some interest.

Code:
  <?php
  #
  # Martin Tournoij <martin@arp242.net>
  # Free for any use. There are no restrictions.
  #
  
  function Query($server, $cmd)
  {
  	# http://wiki.beyondunreal.com/Legacy:UT_Server_Query
  
  	$address = gethostbyname($server['0']);
  	
  	if (!$s = fsockopen('udp://' . $server['0'], $server['1'], $errno, $errstr, 2))
  		return False;
  
  	stream_set_timeout($s, 2);
  	fputs($s, "\\$cmd\\");
  	$info = fread($s, 8192);
  	fclose($s);
  	$info2 = explode('\\', "\\$info\\");
  
  	foreach ($info2 as $k => $v)
  	{
  		if (empty($v) || $v == '' || $v == ' ')
  			continue;
  		if (is_int($k/2))
  			$pv = $v;
  		else
  			$prettyinfo["$pv"] = htmlentities($v);
  
  	}
  	$prettyinfo['str'] = $info;
  
  	return $prettyinfo;
  }
  
  # We do not use a masterserver but maintain the list here manually; This is a 
  # feature and not a bug because the masterserve for the mod I play 
  # (Infiltration) is not particuarly reliable.
  $servers = array(
  	array('cerberon.net', 17778), # coop1
  	array('cerberon.net', 27778), # coop2
  	array('90.184.183.249', 7778), # wargamez
  	array('xplod.de', '7778'), # xplod
  	array('xplod.de', '8889'), # xplod2
  	array('84.255.245.128', '7778') # planetweed
  );
  
  if (isset($_POST['server']))
  	$_GET['server'] = $_POST['server'];
  
  if (isset($_GET['server']))
  {
  	$a = explode(':', $_GET['server']);
  	$servers = array(array($a['0'], $a['1']));
  }
  
  $tbl = array();
  foreach ($servers as $s)
  {
  	$info = Query($s, 'status');
  
  	if (empty($info['str']))
  	{
  		# Just hide the entry, servers seem to be going up & down more often than 
  		# the space shuttle ...
  		#$tbl .= "<tr><td colspan=\"4\">Could not connect to {$s['0']} {$s['1']}</td></tr>";
  		continue;
  	}
  	
  	if (isset($info['numplayers']))
  	{
  		$numplayers = $info['numplayers'] . " Players:<br />\n";
  		$players = Query($s, 'players');
  
  		for ($i=0; $i<=$info['maxplayers']; $i++)
  		{
  			$k = "player_$i";
  			if ($players["$k"])
  				$numplayers .= $players["$k"] . "<br />\n";
  		}
  	}
  	else
  		$numplayers = 'No players';
  
  	# XXX Detect DTAS in a better way
  	if ($info['gametype'] == 'INFCoopUnrealGame')
  		$gametype = 'Coop';
  	elseif ($info['gametype'] == 'INFg_EASGame')
  		$gametype = 'EAS';
  	elseif (stristr($info['hostname'], 'dtas'))
  		$gametype = 'DTAS';
  	elseif ($info['gametype'] == 'InfilTeamGamePlus')
  		$gametype = 'TDM';
  	else
  		$gametype = $info['gametype'];
  
  	$tbl[] = sprintf('<tr>
  		<td><a href="ut-serverbrowse.php?server=%s:%s">%s</a><br />
  		<span class="small">%s:%s</span></td>
  		<td>%s</td>
  		<td>%s</td>
  		<td>%s</td>
  		</tr>',
  		$s['0'], $s['1'], $info['hostname'], $s['0'], $s['1'], $info['mapname'], $gametype, $numplayers);
  }
  
  $tbl = implode('', $tbl);
  if (stristr($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml'))
  	$charset = 'application/xhtml+xml; charset=utf-8';
  else
  	$charset = 'text/html; charset=utf-8';
  header("Content-Type: $charset");
  ?>
  <?php echo '<?xml version="1.0" encoding="UTF-8"?>' ?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  	<head>
  		<meta http-equiv="Content-Type" content="<?php print $charset; ?>" /> 
  		<title>Infiltration Servers</title>
  		<style type="text/css">
  			table { border-collapse: collapse; margin: .25em; width: 95%; }
  			td, th, table, caption { border: 1px solid black; }
  			tfoot td { border: none; font-size: .8em; }
  			caption, th, tfoot { background-color: #ddd; }
  			caption { font-weight: bold; }
  			tr:hover { background-color: #eee; }
  			div { border: 1px solid black; margin: .25em; width: 94%; padding: .2em; margin-top: 2em; }
  			label { width: 6em; }
  			.small { font-size: .75em; margin-left: 2em; }
  			p { width: 40em; }
  		</style>
  	</head>
  	<body>
  		<p>Refresh your browser and the list will be refreshed. Click on a link to 
  			refresh just that one server (faster).<br />
  			Unfortunately, the list of players is often incomplete. This is not a 
  			bug on my side but one in the UT server. Sometimes trying for a few times can get you a 
  			larger list, but often not :(</p>
  
  		<table summary="Infiltration servers">
  			<caption>Infiltration servers</caption>
  			<thead>
  				<tr>
  					<th scope="col">Server</th>
  					<th scope="col">Map</th>
  					<th scope="col">GameType</th>
  					<th scope="col">Players</th>
  				</tr>
  			</thead>
  			<tfoot>
  				<tr>
  					<td colspan="4">List fetched on <?php print date('r'); ?>
  						| <a href="ut-serverbrowse.php.txt">View source</a></td>
  				</tr>
  			</tfoot>
  			<tbody>
  				<?php print $tbl; ?>
  			</tbody>
  		</table>
  
  		<form method="post" action="<?php print $_SERVER['PHP_SELF'] ?>">
  			<div>
  				<label for="server">Manually enter server to query. <em>address:port</em>: </label>
  				<input type="text" id="server" name="server" value="<?php print $_GET['server'] ?>"/>
  				<input type="submit" value="Query" />
  			</div>
  		</form>
  
  		<?php if (isset($_GET['server'])) { ?>
  			<p><a href="<?php print $_SERVER['PHP_SELF'] ?>">Back to main</a></p>
  		<?php } ?>
  
  	</body>
  </html>
__________________
UNIX was not designed to stop you from doing stupid things, because that would also stop you from doing clever things.
Reply With Quote