Archivos de la categoría Programación

Segmentation fault al instanciar un webservice WCF de .NET desde PHP

Recientemente nos hemos encontrado con un problema que nos ha tenido varios días bloqueados hasta conseguir averiguar el origen. Llevamos mucho tiempo utilizando webservices programados en .NET desde aplicaciones PHP sin ningún problema, pero esta vez estaba hecho con la nueva tecnología WCF (Windows Communication Foundation) de Microsoft .NET 3.5. El problema era que en cuanto lo subimos a producción la aplicación PHP devolvía un pantallazo en blanco sin más información. Analizando los logs vimos que el proceso de Apache provocaba un Segmentation Fault con lo que no llegábamos a ver ninguna excepción.

Tras muchas pruebas conseguimos aislar el error en la línea de código que instanciaba el nuevo webservice, si eliminábamos esa parte todo funcionaba correctamente.

$client=new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

Lo más curioso es que en los entornos de preproducción sí que funcionaba, no entendíamos nada. Analizando las máquinas de los distintos entornos todas eran idénticas en cuanto a versiones y configuración excepto algunos parámetros SOAP para PHP, en la máquina de producción tienen la caché de wsdl activada mientras que en los demás está desactivada.

soap.wsdl_cache_enabled = 1
soap.wsdl_cache_dir = /tmp/
soap.wsdl_cache_ttl = 7200
soap.wsdl_cache_limit = 50

No puede ser que el error sea el cacheo. Pues sí, lo es, en cuanto desactivamos la caché del servidor de producción todo comenzó a funcionar correctamente.

Perfecto, pero la caché tiene que estar activada, la aplicación hace uso de unos 35 webservices, si para cada instancia de cada uno de ellos hay que cargar previamente el wsdl, el rendimiento cae por los suelos, es imprescindible.

Pues nada, solución increíblemente cutre:

ini_set('soap.wsdl_cache_enabled', '0');
ini_set('soap.wsdl_cache_ttl', '0');
ini_set('soap.wsdl_cache', '0');

$client = new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

ini_set('soap.wsdl_cache_enabled', '1');
ini_set('soap.wsdl_cache_ttl', '7200');
ini_set('soap.wsdl_cache', '3');

Así es, desactivamos la caché antes de instanciar sólo este webservice y la volvemos a activar después. No hemos encontrado otra manera de solucionarlo ni hemos encontrado ninguna referencia de alguien que haya sufrido el mismo problema. La solución es mala, muy mala, no deja de ser un apaño, pero funciona y nos permite salir del paso hasta que sepamos por qué ocurre.

Aumenta tu productividad integrando el sistema de incidencias en el entorno de desarrollo: Eclipse + Mylyn + TFS

Si hace unas semanas veíamos como integrar las incidencias de Jira en Eclipse, hoy veremos como hacer lo propio con Microsoft Team Foundation Server (TFS).

En determinados proyectos nos vemos obligados utilizar TFS para el control de versiones, pero en nuestro caso lo utilizamos desde Eclipse utilizando un plugin desarrollado por Teamprise. A finales de 2009 Microsoft compró esta empresa con lo que el plugin se llama ahora Microsoft Visual Studio Team Explorer Everywhere 2010, casi nada :P, así que ya es parte de Visual Studio.

El paso final de la integración consiste en poder acceder a las incidencias del TFS desde el sistema integrado en Eclipse, Mylyn. Para conseguirlo Teamprise tiene otro plugin, Teamprise Mylyn Conector.

La instalación es muy sencilla, se descomprime en la carpeta de Eclipse, se reinicia éste y a funcionar. Veamos como configurarlo. Si no visualizamos la lista de tareas en nuestro Eclipse, vamos a “Window -> Show view -> Task list”, nos aparecerá la ventana correspondiente. Haciendo click con el botón derecho vamos a “New -> Query” y en la ventana que se abre seleccionamos “Add task repository“.

En la nueva ventana seleccionamos “Teamprise repository“:

Indicamos los datos del TFS:

Seleccionamos el nuevo repositorio creado:

El proyecto al que necesitamos acceder:

Y el tipo de incidencias a cargar: Eso es todo, en la ventana de “Task list” nos aparecerá la lista de las incidencias:

Tal y como veíamos en el caso de Jira, desde ahí podremos ver las incidencias, modificarlas, crear otras nuevas, asociar contextos, programar las tareas, etc.

¿La pega? Que no permite realizar filtros en las incidencias. Así como cuando vimos la integración con Jira la creación de consultas era muy completa, permitiendo múltiples filtros, en este caso se limita a recoger todas las incidencias del tipo indicado, no pudiendo, por ejemplo, recoger sólo las incidencias o tareas asociadas a nuestro usuario. Es un mal menor, sí, pero limita tu productividad ya que tienes que revisar todas las incidencias una por una hasta encontrar las tuyas. En el propio plugin de TFS el comportamiento es muy parecido, no puedes hacer filtros en las incidencias, como mucho puedes ordenarlas alfabéticamente por el “Asigned to” y localizar las tuyas, poco útil también.

Aumenta tu productividad integrando el sistema de incidencias en el entorno de desarrollo: Eclipse + Mylyn + Jira + Subversion

Hoy voy a hablaros de algo que ha revolucionado mi entorno de trabajo. Hasta ahora tenía por un lado el entorno de desarrollo con Eclipse y por el otro el sistema de incidencias web con Jira (también sirven Trac o Bugzilla). La verdad es que me resultaba muy pesado ir cambiando de uno a otro para revisar las incidencias y más aún para cambiar los archivos necesarios para revisar las distintas incidencias. Cuando digo que me resultaba pesado me refiero a que entonces no me daba cuenta del tiempo que perdía, pero ahora sí.

Para comenzar veremos cómo podemos acceder a las incidencias desde el propio Eclipse sin necesidad de cambiar de aplicación. La magia viene aportada por Mylyn.

Para instalar el plugin necesario para Jira debemos añadir el repositorio donde se encuentra desde

Help->Install new software -> Available sites -> Add

con la url:

http://update.atlassian.com/atlassian-eclipse-plugin/e3.4

Desde este nuevo repositorio seleccionamos los componentes a instalar, probablemente Mylyn ya lo tengas instalado. Esto es lo que debes escoger:

Si en vez de Jira usas Trac o Bugzilla, utiliza el conector adecuado.

Una vez reiniciado Eclipse mostraremos la pestaña de incidencias desde

Window->Show view->Task list

Desde el primer icono de la pestaña vamos a “Add Repository“.

Seleccionas el tipo de repositorio de incidencias que vas a crear.

Y añades los datos de acceso al mismo.

Si todo ha ido bien verás una pantalla similar a ésta donde puedes crear tu primera consulta de incidencias o seleccionar una previamente guardada en el servidor ya que importa tus filtros existentes.

Si decides crear una consulta nueva tienes un formulario con todas las opciones a tu disposición. Por ejemplo, yo he creado una que me lista todas las incidencias reportadas por mi.

Automáticamente irá recuperando del servidor las incidencias que cumplan los requisitos especificados en los filtros.

Haciendo doble click en una incidencia se abre el detalle de la misma con todas sus opciones que podremos modificar a nuestro antojo, añadir comentarios nuevos, adjuntar archivos, etc.

Mylyn aporta al sistema funcionalidades extra, una de ellas es la de programarnos las incidencias en función de nuestra carga de trabajo y tener una previsión de lo que vamos a hacer y cuando. Esta información no se guarda en el servidor, es interna.

A la hora de adjuntar archivos, además de seleccionar los que tengamos en nuestro equipo podremos hacer directamente desde Eclipse una captura de pantalla, recortar el trozo que nos interesa y enviarlo a Jira, todo en uno y sin salir de nuestro entorno de desarrollo.

Llegamos a una de las opciones que realmente hacen que ahorremos muchísimo tiempo, el contexto. Cada incidencia tiene un circulito a su izquierda que permite activar esa tarea. Al activar una tarea el sistema asume que todos los archivos que vayas abriendo mientras la tengas activada están asociados a la resolución de la misma.

Abre algunos archivos de ejemplo y en la pestaña “Context” de la incidencia verás cómo va asociándolos. Si ahora, en vez de cerrar la incidencia en la “X” la desactivas de nuevo en el circulito, verás como automáticamente se cierran también todos los archivos asociados. Y aquí viene lo bueno, si vuelves a activarla se abren automáticamente todos los archivos sobre los que estabas trabajando. Este sistema te permite cambiar rápidamente el entorno de trabajo de una incidencia a otra sin tener que buscar de nuevo todos los fuentes uno a uno.

Hay una opción más para trabajar con los contextos que permite que distintos usuarios los compartan y consiste en subirlos al propio servidor de Jira. Se suben como archivos zip como si fuese un archivo adjunto a la incidencia normal y corriente, pero Mylyn sabe lo que tiene que hacer con ellos cuando se recuperan.

Finalmente otra opción interesante es la integración con Subversion. Teniendo una incidencia activada, cada vez que hagamos commit al repositorio nos adjuntará automáticamente en el comentario del svn información acerca de la incidencia que se resuelve, eso que nunca hacemos :P.

Como veis tenemos el desarrollo junto a las incidencias y la resolución de las mismas en Subversión completamente integrado en una sola aplicación. Cuando os comentaba al principio del artículo sobre que no me daba cuenta de lo pesado que era hacerlo en tres pasos, ahora veis a qué me refería, es una de esas cosas que hasta que las tienes no sabes lo que vale.

Segmentation fault en WordPress al utilizar eAccelerator

Hacía casi un mes que no escribía nada :O, cosas del periodo navideño. Espero retomar mis hábitos de escritura con ganas con el año nuevo, tengo muchas cosas pendientes por contar.

Llevaba un par de meses preocupado por mi blog. Por alguna extraña razón, de vez en cuando comenzaba a devolverme pantallas en blanco al acceder a cualquier artículo e incluso al acceder al tablero de administración, la pantalla de login funcionaba bien pero una vez te autentificabas saltaba el error. Reiniciando el servidor web volvía a funcionar correctamente por un periodo indeterminado de tiempo, a veces unas horas a veces varios días, pero terminaba saltando de nuevo la pantalla en blanco.

Al comprobar los logs de errores de Apache lo único que aparecía era algo como:

[Mon Jan 08 09:34:14 2010] [notice] child pid 4899 exit signal Segmentation fault (11)

Es decir, no me aportaba nada, un misterio, y tampoco encontré ninguna referencia útil buscando “wordpress segmentation fault“. Me tocó poner sobre la mesa todas mis habilidades de depuración y a base de echo’s y exits ir siguiendo la pista hasta llegar al punto donde saltaba el error. Tras un par de horas llegué a la conclusión de que el problema estaba en la función wp_filter_kses del archivo:

wp-includes/kses.php

Faltaba saber por qué provocaba un Segmentation fault y no un error de PHP estándar. Descubrí además que reescribiendo el mismo archivo sin hacer cambios, es decir, cambiando la fecha de actualización, todo comenzaba a funcionar correctamente… hasta que volvía a fallar. Raro, raro, raro…

Teniendo ya una referencia clara del origen del problema, una sencilla búsqueda de “kses.php segmentation fault”  me condujo al origen del problema, y éste no era otro que el sistema de cacheo que se utiliza en el servidor, eAccelerator. Al parecer, y sin una causa lógica, la caché de ese archivo se corrompe periódicamente y al intentar leerlo provoca el “Segmentation fault“. Tiene sentido entonces que al cambiar la fecha del archivo volviese a funcionar, este cambio provocaba que se regenerase la caché del mismo y funcionase correctamente.

La solución es indicarle a eAccelerator que no cachee ese archivo concreto. Para ello añadimos al archivo de configuración:

/etc/php.d/eaccelerator.ini

eaccelerator.filter="!*kses.php"

Con esto se acabaron los pantallazos en blanco, espero recuperar algo de las visitas perdidas las últimas semanas ya que ha estado la mayor parte de las vacaciones caído por culpa de este error.

De artículo a proyecto explicado paso a paso: acorta URLs con IraUrl.me

Hace un par de semanas me encontré con la necesidad de utilizar uno de esos sistemas que hay por ahí para acortar URL‘s. Necesitaba enviar una dirección por SMS y tenía que ocupar la menor cantidad de caracteres posible por aquello de optimizar el texto del mensaje. Mientras lo utilizaba pensaba en lo ingenioso de utilizar un sistema de numeración base36 para reducir exponencialmente el número de caracteres de la redirección. Esto iba a ser, pues, un artículo sobre las ventajas de los sistemas de numeración distintos al decimal para determinados proyectos, pero se acabó convirtiendo en un proyecto completo. Cuando estaba comenzando la explicación teórica pensé, ¿por qué no hacerlo? ¿por qué no demostrar lo rápido y fácil que se puede montar algo en Internet hoy en día?

Así, tras unas 15 horas de trabajo os presento IraUrl.me, un acortador de URL’s al estilo de TinyUrl o Bit.ly. Me ha costado más escribir el artículo que hacerlo realidad, curioso ¿eh?. En realidad a medida que iba preparando la aplicación se me iban ocurriendo más cosas que sería interesante montar, por lo que las 8 horas iniciales, más o menos, se convirtieron en 15.

iraurl

La teoría

Para el que no lo sepa, un acortador de URL se basa en encontrar un dominio lo más corto posible y crear redirecciones HTTP 301 a otras URL‘s. El truco está en optimizar los parámetros que añadiremos a la URL para que sean lo más cortos posible, no queremos que éstos nos penalicen lo corto del dominio.

¿Cómo funcionan entonces estos acortadores de URL‘s? Mucho más fácil de lo que parece y seguramente como a ti se te habría ocurrido. Simplemente tenemos una base de datos donde vamos añadiendo registros a una tabla a medida que se van creando nuevas URL’s cortas.  Esta tabla tiene un campo autonumérico, la clave de la tabla, que para cada nueva URL nos devuelve un identificador único, con lo que cada dirección podría ser accesible de la manera habitual:

http://dominio.com/1

http://dominio.com/1000000

Esa es exactamente la idea, lo único que hacemos es cambiar el identificador en cuestión de base10 (la de nuestro sistema métrico decimal) a base36 o base62 en mi caso. Otros sistemas de numeración conocidos son el hexadecimal (base16) y base64.

Vale, ya has hablado en chino. ¿De qué va esto? Veamos.

Sobre bases de numeración

El sistema decimal utiliza diez dígitos (de ahí lo de decimal :P) para formar todas las combinaciones de números posibles. Lo que ya conocemos, vamos. El binario utiliza dos dígitos (0 y 1), el hexadecimal 16 (0..9ABCDE), base36, como su nombre indica, treinta y seis (0..9a..z) y base62 utiliza los 62 dígitos que comprenden los números del 0 al 9 y las letras de la A a la Z en mayúsculas y minúsculas (0..9a..zA..Z). Veamos unos ejemplos:

Binario Decimal Hexadecimal Base36 Base62
0 0 0 0 0
1 1 1 1 1
10 2 2 2 2
1010 10 A a a
1100100 100 64 2s 1c
1000000 F4240 lfls 4c92
10000000 989680 5yc1s FXsk

Se puede observar de un vistazo cómo a medida que aumenta el número, cuanto mayor sea la base que manejamos menos dígitos tendrá . Los números, a fin de cuentas, son combinaciones continuas entre todos los dígitos posibles.Así, en función de la base y del número de dígitos, el mayor número representable representable sería:

Num. dígitos
Decimal Base62
1 10 62
2 100 3844
3 1000 238328
4 10000 14776336
5 100000 916132832
6 1000000 56800235584
7 10000000 3521614606208
8 100000000 218340105584896
9 1000000000 13537086546263552

O lo que es lo mismo, base(número de dígitos), 629 contra 109.Espero que se entienda la teoría. Como curiosidad:

Decimal: 10000000000000000000000

Base62: 36aHo5IWaicak

La pregunta ahora sería, ¿Por qué Base62 y no Base64, por ejemplo, mucho más conocida? Sencillo, porque además de los 62 caracteres de Base62, Base64 utiliza dos adicionales, generalmente + y / además del =, lo que convierten la cadena en no web safe, es decir, los caracteres especiales debieran traducirse para que su transporte no diese problemas, con lo que estaríamos perdiendo las ventajas de nuestro cifrado corto. Los 62 caracteres utilizados en Base62 son totalmente seguros, sólo letras (mayúsculas y minúsculas) y números.

Sabiendo ya cómo funciona el sistema, veremos cómo crear nuestra aplicación. Obviamente no contaré todo paso a paso ya que sino tardaría mucho más en escribir el artículo que en hacer la aplicación, me meteré sólo en las cosas que considere más importantes.

Para codificar/decodificar de base10 a base62 utilizaré estas librerías:

function dec2base($dec, $base, $digits = FALSE) {
      if($base < 2 or $base > 256) {
            die("Invalid Base: .$basen");
      }
      bcscale(0);
      $value = '';
      if(!$digits) {
            $digits = digits($base);
      }
      while($dec > $base - 1) {
            $rest = bcmod($dec,$base);
            $dec = bcdiv($dec,$base);
            $value = $digits[$rest].$value;
      }
      $value=$digits[intval($dec)].$value;
      return (string) $value;
}

function base2dec($value, $base, $digits = FALSE) {
      if($base < 2 or $base > 256) {
            die("Invalid Base: .$basen");
      }
      bcscale(0);
      if($base < 37) {
            $value = strtolower($value);
      }
      if(!$digits) {
            $digits = digits($base);
      }
      $size = strlen($value);
      $dec = '0';
      for($loop=0; $loop < $size; $loop++) {
            $element = strpos($digits, $value[$loop]);
            $power = bcpow($base, $size-$loop-1);
            $dec = bcadd($dec, bcmul($element, $power));
      }
      return (string)$dec;
}

function digits($base) {
      if($base < 64) {
            return substr('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', 0, $base);
      } else {
            return substr("x0x1x2x3x4x5x6x7x8x9xaxbxcxdxexfx10x11x12x13x14x15x16x17x18x19x1ax1bx1cx1dx1ex1f !x22#x24%&'()*+,-./0123456789:;<=>x3f@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~x7fx80x81x82x83x84x85x86x87x88x89x8ax8bx8cx8dx8ex8fx90x91x92x93x94x95x96x97x98x99x9ax9bx9cx9dx9ex9fxa0xa1xa2xa3xa4xa5xa6xa7xa8xa9xaaxabxacxadxaexafxb0xb1xb2xb3xb4xb5xb6xb7xb8xb9xbaxbbxbcxbdxbexbfxc0xc1xc2xc3xc4xc5xc6xc7xc8xc9xcaxcbxccxcdxcexcfxd0xd1xd2xd3xd4xd5xd6xd7xd8xd9xdaxdbxdcxddxdexdfxe0xe1xe2xe3xe4xe5xe6xe7xe8xe9xeaxebxecxedxeexefxf0xf1xf2xf3xf4xf5xf6xf7xf8xf9xfaxfbxfcxfdxfexff", 0, $base);
      }
}

function base_encode($value) {
      return dec2base(base2dec($value, 256), 62);
}

function base_decode($value) {
      return dec2base(base2dec($value, 62), 256);
}

Las dos últimas funciones son las que utilizaremos para las conversiones.

Paquetes y librerías utilizadas:

  • Free CSS Templates: Para tener una bonita plantilla xhtml para nuestro proyecto 🙂 .
  • Maxmind GeoLite Country: Para la geolocalización de un usuario a través de su IP.
  • Wurfl: Para identificar el navegador/terminal de un visitante por su User Agent. Yo lo complemento con Tera-Wurfl para mantener la información en una base de datos.
  • Fusion Charts Free: Para los gráficos de estadísticas.
  • Zero Clipboard: Para copiar al portapapeles la url corta generada sin que el usuario deba seleccionarla, solo con un click.
  • JqueryUI: Para el componente de navegación con pestañas.
  • Google Safebrowsing API: Para comprobar si una url es potencialmente peligrosa.
  • Adodb (opcional): Para abstraer el acceso a la base de datos. Yo suelo utilizarla en todos mis proyectos pero se pueden utilizar las funciones nativas de PHP.
  • PHPExcel: Para generar Excel y PDF.

Adicionalmente:

  • Un dominio y hosting donde alojarlo (6€).
  • PHP y MySQL (tampoco son obligatorios, puedes hacerlo con cualquier tecnología).
  • 15 horas de tu tiempo :P.

Estructura de la web

Cualquier proyecto web que se precie debe comenzarse describiendo qué queremos mostrar a nuestros visitantes, hay que recopilar todas las ideas, decidir las que interesan de verdad, estudiar cómo se van a disponer en el frontend y terminar con un mapa web que nos indique el flujo a seguir en el trabajo. Este será el nuestro:

mapaWebIraUrlCuando un usuario vaya a una de nuestras url’s cortas en realidad estaremos reenviando la petición http internamente a un script encargado de hacer todo el proceso, link.php en mi caso.

Para ver las estadísticas de una URL me ha gustado el sistema de bit.ly, así que se lo copiamos :P. Añadiendo un “+” al final de la URL corta, en vez de saltar a la dirección larga mostraremos las estadísticas. Esto lo haremos, como en el caso anterior, dirigiendo internamente a otro script, stats.php.

Si el identificador que pretendemos usar para saltar a la url larga o ver estadísticas no existe, reenviaremos a index.php para que muestre un mensaje de error tipo “La url no existe“.

El dominio

Obviamente tendremos que buscar un dominio lo más corto posible, la mayoría estarán ya ocupados, pero buscando y buscando en TLD‘s extraños puedes encontrar algo. Yo he escogido un .me porque tiene un carácter menos que un .com 🙂 y no cuesta lo que un .es :P.

La base de datos

Muy sencilla, dos tablas solamente, en una mantendremos las urls generadas y en otra las estadísticas de acceso a las mismas.

CREATE TABLE IF NOT EXISTS `urls` (
  `id` bigint(20) NOT NULL auto_increment,
  `url` varchar(500) NOT NULL,
  `titulo` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `stats` (
`id` bigint(20) NOT NULL auto_increment,
`idurl` bigint(20) NOT NULL,
`codpais` varchar(255) NOT NULL,
`referer` varchar(255) NOT NULL,
`hostreferer` varchar(255) NOT NULL,
`ua` varchar(255) NOT NULL,
`hora` datetime NOT NULL,
`pais` varchar(255) NOT NULL,
`marca` varchar(255) NOT NULL,
`modelo` varchar(255) NOT NULL,
PRIMARY KEY  (`id`),
KEY `idurl` (`idurl`,`hora`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

La configuración de Apache

Como hemos comentado queremos que cuando alguien vaya a cualquier url de nuestro site del tipo

http://tudominio.com/prueba3

Se redirija internamente al script link.php que será el encargado de procesar la petición. De igual modo si alguien visita

http://tudominio.com/prueba3+

le mostraremos las estadísticas de esa URL (si existen). Configuramos Apache para que tenga en cuenta todas estas particularidades, mod_rewrite será nuestro amigo para conseguirlo. En mi caso he hecho que si la llamada no es un script php, ni una imagen ni un archivo javascript ni un css ni tiene el signo “+“, se vaya a link.php. Si tiene el signo “+” se irá a stats.php.

RewriteEngine on
RewriteCond %{REQUEST_URI} !^/(.*).php$
RewriteCond %{REQUEST_URI} !^/css/(.*)$
RewriteCond %{REQUEST_URI} !^/js/(.*)$
RewriteCond %{REQUEST_URI} !^/(.*)+$
RewriteCond %{REQUEST_URI} !^/images/(.*)$
RewriteRule ^(.+) /link.php?id=$1
RewriteCond %{REQUEST_URI} ^/(.*)+$
RewriteRule ^(.+) /stats.php?id=$1

Imagen y diseño

Para el diseño, o te lo haces tú mismo o si eres un negado creativo como yo te descargas una plantilla superchula de freecsstamplates.org y la adaptas a tus necesidades, no voy a entrar en más detalles.

Crear urls cortas

El primer script de nuestra aplicación. Un sencillo formulario donde el usuario introduce la URL que quiere acortar y al hacer submit… la acortamos integrando el sistema con la comprobación de malware que explicaba hace unos días ;).

$urlbase="";
if(isset($_POST['url'])){
        $url=$_POST['url'];

        try{
	        if(substr($url, 0, 4)!="http")
	        	$url="http://".$url;
			$class = new GoogleSafeBrowsing('ABQIAAAAQYvf-54bCBMuGY20SeONVxQ236Mc_IjryQBl-W_Repaw3fCykA', true);
			$nomalware=$class->lookupsFor($url);

			if($nomalware){
        			$htmltitle="";
        			$html=file_get_contents($url);
        			if($html!=""){
        				preg_match('/(.*)</title>/is', $html, $matches);
					if(is_array($matches) && count($matches>0))
						$htmltitle=trim($matches[1]);
        			}
                		$query="select * from urls where url='$url'";
				$rs=$conn->Execute($query);
				if($rs->recordcount()>0){
					$id=$rs->fields['id'];
				}else{
					$query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
					$rs=$conn->Execute($query);
					$id=$conn->insert_id();
				}
				$base=base_encode($id);
				$urlbase="http://iraurl.me/".$base;
			}else{
				$err=4;
			}
        }catch(exception $e){
        	$err=3;
        }
}

Hemos añadido una pequeña comprobación. Si la URL que se quiere añadir ya existe, devolvemos la misma URL corta, yo he tomado esa decisión, tú puedes hacer lo que quieras. Además obtenemos el título de la URL final para tener una referencia hacia la misma, cuestión de sencillez visual :P.

Reenvío a urls largas

Ya tenemos nuestra URL corta, vamos ahora a reenviar las solicitudes a ella a la larga. Recordemos que nuestro Apache nos va a redirigir esa petición a link.php?id=XXXX. Nuestro script actualiza, además, las estadísticas de visitas de la url.

if(isset($_GET['id'])){
	$idb=$_GET['id'];
	$id=base_decode($idb)+0;
	try{
		$query="select * from urls where id=$id";
		$rs=$conn->Execute($query);
		if($rs->recordcount()>0){
			$url=$rs->fields['url'];
			$referer=@$_SERVER['HTTP_REFERER'];
			$ua=@$_SERVER['HTTP_USER_AGENT'];
			$ip=@$_SERVER['REMOTE_ADDR'];

			$hostreferer="";
			if(preg_match('@^(?:http://)?([^/]+)@i', $referer, $matches)>0)
				$hostreferer = $matches[1];

			$terminal=getMarcaModelo($_SERVER['HTTP_USER_AGENT']);
			$marca=$terminal['marca'];
			$modelo=$terminal['modelo'];

			$temp=getGeoCodeAndPais($ip);
			$codpais=$temp['code'];
			$pais=$temp['pais'];

			$query="insert into stats (idurl, codpais, referer, ua, hora, pais, marca, modelo, hostreferer) VALUES
					($id, '$codpais', '$referer', '$ua', now(), '$pais', '$marca', '$modelo', '$hostreferer')";
			$rs2=$conn->Execute($query);

			header("HTTP/1.x 301 Moved");
			header("Location: $url");
			exit;
		}else{
			header("Location: http://iraurl.me/index.php?err=1");
			exit;
		}
	}catch(exception $e){
		header("Location: http://iraurl.me/index.php?err=2");
		exit;
	}
}
header("Location: http://iraurl.me/index.php?err=1");

Como veis, si la URL no existe redirigimos al usuario a index.php con un mensaje de error. Necesitaremos dos funciones adicionales, las que nos devuelven información del país de origen de una IP y los datos del terminal del usuario (móvi o web). No entraré en detalles sobre la instalación de Maxmind GeoLite Country o de Wurfl/Tera-Wurfl.

function getGeoCodeAndPais($ip){
	require_once(dirname(__FILE__)."/geoip/geoip.inc");
	$gi = geoip_open("/usr/share/GeoIP/GeoIP.dat",GEOIP_STANDARD);
	$codpais=geoip_country_code_by_addr($gi, $ip);
	$pais=geoip_country_name_by_addr($gi, $ip);
	geoip_close($gi);
	return array("pais"=>$pais, "code"=>$codpais);
}

function getCapabilities($ua){
	require_once(dirname(__FILE__)."/Tera-WURFL/TeraWurfl.php");
	$wurflObj = new TeraWurfl();
	$matched = $wurflObj->GetDeviceCapabilitiesFromAgent($ua);
	$movil = $wurflObj->capabilities;

	return $movil;
}

Estadísticas

La teoría es la misma. Si existe la URL cargamos los datos, si no redirigimos a la home. En nuestra caso utilizamos el componente de pestañas de JqueryUI para organizar los distintos tipos de datos que permitiremos ver y añadiremos los botones para exportar a Excel y PDF.

$idb=substr($_GET['id'], 0, strlen($_GET['id'])-1);

$id=base_decode($idb)+0;
$query="select * from urls where id=$id";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
	$urlbase="http://iraurl.me/".$idb;
	$url=$rs->fields['url'];
	$id=$rs->fields['id'];
	$htmltitulo=$rs->fields['titulo'];
	if($htmltitulo=="")
		$htmltitulo=$url;
	$query="select count(*) as nregs from stats where idurl=$id";
	$rs=$conn->Execute($query);
	$clicks=$rs->fields['nregs'];
}else{
	header("Location: http://iraurl.me/index.php?err=1");
	exit;
}

Muy sencillo.

stats

Lo complicado en este caso es mostrar las gráficas con FusionCharts. Para cada una debemos añadir algo de código html:

<div id="chartClicks"></div>
<script type="text/javascript">
 var myChart = new FusionCharts("images/Charts/FCF_Column3D.swf", "idChartClicks", "430", "400", "0", "1");
 myChart.setDataURL(escape("xml.php?t=cli&id='.$idb.'"));
 myChart.setTransparent(true);
 myChart.render("chartClicks");
</script>

El script xml.php será el que devuelva los datos en el formato adecuado para FusionCharts. Por ejemplo:

$query="select DAY(hora) as dia, MONTH(hora) as mes, YEAR(hora) as ano, count(*) as nclicks
 from stats
 where idurl=$id
 group by ano, mes, dia
 order by hora";
$rs=$conn->Execute($query);

$xml='<graph caption="Clicks" rotateNames="1" xAxisName="Día" yAxisName="Clicks" showNames="1" decimalPrecision="0" formatNumberScale="0" chartLeftMargin="5" chartRightMargin="5" chartTopMargin="0">';
while($r=$rs->fetchrow()){
 $xml.='<set name="'.$r['dia']."/".$r['mes']."/".$r['ano'].'" value="'.$r['nclicks'].'" color="#A1A1A1" />';
}
$xml.='</graph>';

Os doy sólo un ejemplo, el resto lo montáis por vuestra cuenta :).

Descifrar urls cortas

Todos los sistemas de acortar URL’s funcionan tal y como cuento en este artículo, haciendo un HTTP/301 redirect hacia la url original.

A partir de la URL corta podemos saber cual es la URL original simplemente siguiendo las redirecciones que hace. Muy sencillo con PHP y que, además nos sirve para, integrándola en nuestra API de malware, prevenir posibles problemas con la URL final.

function get_web_page( $url )
{
    $options = array( 'http' => array(
        'user_agent'    => 'spider',
        'max_redirects' => 10,
        'timeout'       => 120,
    ) );
    $context = stream_context_create( $options );
    $page    = @file_get_contents( $url, false, $context );

    $result  = array( );
    if ( $page != false )
        $result['content'] = $page;
    else if ( !isset( $http_response_header ) )
        return null;    // Bad url, timeout

    // Save the header
    $result['header'] = $http_response_header;

    // Get the *last* HTTP status code
    $nLines = count( $http_response_header );
    for ( $i = $nLines-1; $i >= 0; $i-- )
    {
        $line = $http_response_header[$i];
        if ( strncasecmp( "HTTP", $line, 4 ) == 0 )
        {
            $response = explode( ' ', $line );
            $result['http_code'] = $response[1];
            break;
        }
    }

    return $result;
}

$url="";
if(isset($_POST['url'])){
	$url=$_POST['url'];
	$datos=get_web_page( $url );
	if($datos){
		$headers=$datos['header'];
		$urls=array($url);
		foreach($headers as $head){
			$temp=explode(" ", $head);
			if(strtolower($temp[0])=="location:"){
				$urls[]=$temp[1];
			}
		}
		$htmltitle="";
	    	preg_match('/(.*)</title>/is', $datos['content'], $matches);
		if(is_array($matches) && count($matches>0))
			$htmltitle=trim($matches[1]);
	}
}

Ya está, en $urls tendremos la lista de urls que van saltando hasta llegar a la final.

descifrar

Api

Hoy en día todo tiene que tener Api. Para las estadísticas es muy sencillo, el propio XML que generamos para consumir con FusionCharts nos permite que clientes externos se alimenten del mismo. Para crear URL‘s cortas remotamente, simplemente creamos un archivo api.php:

if(isset($_GET['url'])){
	$url=urldecode($_GET['url']);
	try{
        $htmltitle="";
        if(substr($url, 0, 4)!="http")
        	$url="http://".$url;
        $html=file_get_contents($url);
        if($html!=""){
        	preg_match('/(.*)</title>/is', $html, $matches);
			if(is_array($matches) && count($matches>0))
				$htmltitle=trim($matches[1]);
        }
        $query="select * from urls where url='$url'";
        $rs=$conn->Execute($query);
        if($rs->recordcount()>0){
                $id=$rs->fields['id'];
        }else{
                $query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
                $rs=$conn->Execute($query);
                $id=$conn->insert_id();
        }
        $base=base_encode($id);
        $urlbase="http://iraurl.me/".$base;
        echo $urlbase;
    }catch(exception $e){
   		echo "ERROR";
    }
}

Eso es todo. No olvides integrarlo también con el sistema de malware.

Conclusiones

Bueno, y todo este rollo ¿para qué?. Pues muy sencillo, para que veais que hoy en día la tecnología está al alcance de todos, es sencillo y rápido crear un proyecto en Internet, hay de todo por todas las esquinas, la tecnología no es lo importante, lo que verdaderamente cuenta es cómo mueves ese producto tecnológico para rentabilizarlo y obtener un beneficio de él.

Ya tengo mi proyecto superchulo funcionando, sólo me ha costado unas 15 horas de trabajo. Le he puesto un poco de Adsense por aquí y por allí. ¿Y ahora qué? ¿A esperar a que la gente entre y me haga millonario? 😛 Es mucho más complicado que eso como todos sabéis, primero tienes que tener una masa de usuarios elevada que le dé movimiento al proyecto y después tienes que conseguir que la mayoría de ellos sean gente normal, no gente técnica, usuarios avanzados que no pagamos por nada ni pinchamos en publicidad :P.

Hoy en día, en Internet, como en cualquier negocio, las técnicas de marketing y venta son mucho más importantes que la tecnología en sí misma, es duro reconocerlo, pero es así. De nada sirve que tengas el mejor producto del mundo mundial si no consigues que la gente lo utilice y se deje dinero, así de claro. Si tienes los conocimientos adecuados para mover el negocio, no te preocupes, la tecnología te la aporta cualquier partner por un módico precio, pero poner en manos de otro toda la estrategia de ventas de tu negocio no está tan claro ¿no?.

Espero que os sirva de algo el artículo. He querido mostrar, fundamentalmente, cómo utilizando algunas librerías que puedes obtener sin coste puedes hacer algo realmente útil y funcional con muy poco esfuerzo. Seguro que sacáis alguna idea.

Perdón por el rollo :P, al final me ha costado mucho más escribir el artículo que implementarlo.

Google Safe Browsing API con PHP, filtra las urls potencialmente peligrosas

Preparando un pequeño nuevo proyecto del que os hablaré en mi siguiente artículo, tuve un problema que me llevó a escribir este otro. En un momento de la aplicación el usuario debe escribir una url (el resto es secreto aún :P). Pues bien, ya durante las pruebas aparecieron los típicos graciosos que utilizan url’s potencialmente peligros, de esas que no hacen cosas nada buenas. Buscando un poco dí con Google Safe Browsing API, el sistema que Google pone a nuestra disposición para comprobar contra sus sistemas si tienen una determinada url recogida dentro de su base de datos de urls peligrosas, la misma que utilizan ellos para lanzar a veces esos avisos de “¿seguro que quieres ver esto?”.

Esta API funciona de modo diferente a la mayoría de aplicaciones de Google que residen directamente en la nube y lanzamos consultas contra su API. En este caso nos provee de su base de datos de urls potencialmente peligrosas para que las guardemos localmente y hagamos las consultas directamente en nuestros sistemas, lo único que debemos hacer es actualizar periódicamente esos datos, eso sí, con algunas limitaciones que nos impone Google, como que no se pueden renovar en intervalos inferiores a 30 minutos. Actualmente esta base de datos tiene más de trescientos mil registros.

Vamos a explicar como utilizarla. Lo primero que debemos hacer es, como en todo lo que hace Google, crear una “API KEY” y una sencilla tabla en nuestro servidor MySQL donde alojaremos la información que nos envíe Google.

CREATE TABLE IF NOT EXISTS `malware` (
`malware_hash` varchar(32) NOT NULL,
PRIMARY KEY  (`malware_hash`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Creamos ahora una tarea que se ejecutará periódicamente y que leerá, siguiendo las especificaciones adecuadas, los datos que devuelve Google y los insertará/eliminará de nuestra base de datos. Yo, como ya he comentado en otras ocasiones, utilizo Adodb para el acceso a datos.

include(dirname(dirname(__FILE__))."/includes/funciones.inc.php");

$api_key = "TU KEY";
$google_url = "http://sb.google.com/safebrowsing/update";

//open the remote URL
$target = "$google_url?client=api&apikey=$api_key&version=goog-malware-hash:1:-1,goog-black-hash:1:-1";

$handle = fopen("$target", 'r')
	or die("Couldn't open file handle " . $target);
if ($handle) {
    while (!feof($handle)) {
        $line = fgets($handle);
        if (substr($line,0,1) != '[') {
        	$operation = (substr($line,0,1)); //get the '+' or '-'
        	$hash = trim(substr($line,1)); //get the md5 hash
        	$hash = qstr($hash); //escape
        	if ($operation == '+'){
        		$sql = "insert ignore into malware (malware_hash) VALUES ('".$hash."')";
        		$conn->Execute($sql);
        	}else{
        		$sql = "delete from malware where malware_hash = '".$hash."'";
        		$conn->Execute($sql);
        	}
        }
    }
    fclose($handle);
}

Lo que hacemos simplemente es eliminar los hash que nos indica que debemos quitar y añadir los nuevos. Vamos a probar el sistema. Utilicé varias clases y sistemas que había por ahí para utilizar con PHP pero ninguno me parecía lo suficientemente completo hasta que encontré este. Su funcionamiento es muy sencillo. De nuevo está hecho usando Adodb, cámbialo según tus requerimientos.

$class = new GoogleSafeBrowsing('tu_key', true);
$safe=$class->lookupsFor("http://blog.osusnet.com");

Nos devolverá un booleano que indica si la url es segura o no. Si probamos con alguna de éstas obtendremos un bonito false :). Espero que os sirva.

Monitorizando aplicaciones web con rrdtool

Vamos con uno de esos artículos que le gustan a Álvaro basados en experiencias del trabajo.

En uno de los proyectos en los que estamos trabajando actualmente y del que ya os he hablado, teníamos un problema de rendimiento en una aplicación web, pero no teníamos localizado el punto donde se nos iba el tiempo. Incluso había momentos que la aplicación iba bien pero en otros era lamentable. Se nos ocurrió, entonces, hacer un sencillo sistema de monitorización de los distintos puntos sensibles que manejábamos y crear con ellos una serie de gráficas que nos permitiese hacer un seguimiento visual de los potenciales problemas.

Nuestra aplicación se basa en llamadas a distintos webservices a partir de las cuales se genera un XML al que se le aplican transformaciones XSL para obtener el código html que se muestra al usuario. Algunas de esas llamadas se guardan en la sesión del usuario, con lo que en sucesivos accesos a la aplicación el rendimiento es infinitamente mejor, así que necesitábamos que el control se hiciese con sesiones limpias para que no se nos desvirtuasen las gráficas.

Nuestro sistema consiste en generar por un lado, en la aplicación web, una serie de datos estadísticos que nos ayuden a buscar los puntos conflictivos. Estos datos se insertarán periódicamente en una base de datos rrd (round robin database) que finalmente utilizaremos para generar las gráficas.

Preparando la aplicación web

El primer paso es, entonces, modificar el código de la aplicación para que vaya generando los tiempos de respuesta en los distintos puntos que queremos controlar. Para facilitar las cosas decidimos que cada vez que se pase un parámetro predefinido en la url, en vez de mostrar la página html correspondiente mostraremos las estadísticas que después controlaremos. En nuestro caso decidimos que la url de estadísticas fuese:

http://tudominio.com/index.php?stats

Es decir, añadiendo el parámetro stats a cualquiera de las urls del site obtendremos los tiempos de respuesta que necesitamos.

Tenemos una función que genera el tiempo exacto del servidor cada vez que se llama, así que la utilizaremos después de ejecutar cada uno de los webservices y guardaremos esos datos. En mi caso tengo que obtener ocho puntos de control además del tiempo de inicio y el tiempo final, algo así:

function getTime() {
 $mtime2 = explode(" ", microtime());
 $endtime = $mtime2[0] + $mtime2[1];
 return $endtime;
}
$isStats=isset($_GET['stats']);
if($isStats)
    $time_inicio=getTime();
....
if($isStats)
    $time_ws1=getTime();
.....
if($isStats)
    $time_ws2=getTime();
....
if($isStats)
    $time_ws3=getTime();
....
if($isStats)
    $time_ws4=getTime();
....
if($isStats)
    $time_ws5=getTime();
....
if($isStats)
    $time_ws6=getTime();
....
if($isStats)
    $time_ws7=getTime();
....
if($isStats)
    $time_ws8=getTime();
....
if($isStats)
    $time_final=getTime();
......

Finalmente devolvemos una línea con todos los valores intermedios, es decir, para cada indicador sería el valor del tiempo de ese indicador menos el del anterior, esto nos dará el valor real del tiempo que ha pasado entre el indicador anterior y el actual. Para facilitar la posterior integración de estos valores con rrdtool lo que haremos será generarlos ya en el formato adecuado, separando cada valor con “:”.

if(isset($_GET['stats'])){
	echo number_format($time_ws1-$time_inicio, 7).":".
			number_format($time_ws2-$time_ws1, 7).":".
			number_format($time_ws3-$time_ws2, 7).":".
			number_format($time_ws4-$time_ws3, 7).":".
			number_format($time_ws5-$time_ws4, 7).":".
			number_format($time_ws6-$time_ws5, 7).":".
			number_format($time_ws7-$time_ws6, 7).":".
			number_format($time_ws8-$time_ws7, 7).":".
			number_format($time_final-$time_inicio, 7);
}

Si ahora llamamos a nuestra url obtendremos los valores de nuestros indicadores. El último valor nos devolverá el tiempo completo de ejecución del script entre los valores $time_inicio y $time final.

0.0281749:0.5443010:0.3132501:2.9015441:0.0000241:0.6517198:5.5171580:0.0000379:10.0677590

Vemos cómo esta llamada ha tardado diez segundos, de los cuales 5,5 corresponden al indicador ws7.

Tenemos el primer paso preparado, vamos ahora a crear la base de datos donde almacenaremos toda la información estadística de la url. Tendremos una tarea que se ejecutará cada minuto y que recuperará los valores en ese momento de los identificadores que se han definido.

Creando la base de datos

Para guardar la información he escogido rrd, el mismo que utilizan aplicaciones como mrtg o Cacti y el utilizado en la mayoría de sistemas de monitorización. Según reza en su propia web, es el sistema estándar para gráficas y logs de series de datos temporales.

Para que todo vaya bien nuestra base de datos rrd debe tener la misma cantidad de fuentes de datos que el sistema de estadísticas, nueve en este caso, los ocho puntos de control más el total. Además tenemos que pensar qué graficas vamos a crear. Yo quiero cuatro: última hora, últimas seis horas, últimas 24 horas y última semana. No necesito hacer un sistema con histórico avanzado, lo que quiero en este caso es controlar el rendimiento, así que no necesitaré gráficas mensuales ni anuales, pero se podrían generar de igual modo.

Para crear nuestro archivo rrd ejecutamos el comando siguiente:

rrdtool create estadisticas.rrd --step 60
DS:ws1:GAUGE:120:0:U
DS:ws2:GAUGE:120:0:U
DS:ws3:GAUGE:120:0:U
DS:ws4:GAUGE:120:0:U
DS:ws5:GAUGE:120:0:U
DS:ws6:GAUGE:120:0:U
DS:ws7:GAUGE:120:0:U
DS:ws8:GAUGE:120:0:U
DS:total:GAUGE:120:0:U 

RRA:AVERAGE:0.5:1:60
RRA:AVERAGE:0.5:1:360
RRA:AVERAGE:0.5:5:144
RRA:AVERAGE:0.5:10:202
RRA:MAX:0.5:1:60
RRA:MAX:0.5:1:360
RRA:MAX:0.5:5:144
RRA:MAX:0.5:10:202
RRA:MIN:0.5:1:60
RRA:MIN:0.5:1:360
RRA:MIN:0.5:5:144
RRA:MIN:0.5:10:202

Vamos a explicarlo un poco.

  • –step=60: indica que los datos los actualizaremos cada 60 segundos. Nuestro sistema guardará estadísticas cada minuto.
  • DS:ws1:GAUGE:120:0:U: Fuentes de datos, hay una línea de este tipo para cada valor que vamos a mantener.
    1. wsX es el nombre que tendrá esa fuente.
    2. GAUGE es el tipo de fuente de datos, puedes consultar la documentación completa para tener más idea.
    3. 120 es el parámetro heartbeat, tiempo máximo entre dos actualizaciones antes de que se asuma que es desconocido. He puesto el doble de step, si pasado ese tiempo no hay una actualización es que se ha perdido por cualquier razón.
    4. Los últimos datos son los valores mínimo y máximo que habrá en la fuente de datos. En nuestro caso el mínimo será cero y el máximo no lo sabemos, le ponemos U de unknown.
  • RRA:AVERAGE:0.5:1:60: Round robin archives. El objetivo de una rrd es guardar datos en los rra que mantienen valores y estadísticas para cada una de las fuentes de datos definidas.
    1. AVERAGE/MAX/MIN/LAST: la función utilizada para consolidar los datos. Como su propio nombre indica son la media, máximo, mínimo y el último valor.
    2. 0.5; el xff (xfiles factor). De 0 a 1, indica el número de datos UNKNOWN que se permiten por intervalo de consolidación.
    3. 1: define el número de datos utilizados para crear un intervalo de consolidación.
    4. 60: Cuantos datos se mantienen en cada RRA. Esto depende de la gráfica que vamos a generar y se relaciona con el dato anterior.

La última parte es la más complicada de comprender. Intentaré explicarlo lo mejor que pueda para que se entienda. En nuestro caso tomamos un dato cada 60 segundos y queremos tener la gráfica de la última hora (60 segundos x 60 minutos, 3600 segundos). Necesitamos, por tanto, 60 puntos de control (3600/60 segundos de intervalo), utilizaremos cada uno de los datos que se toman. Esto es lo que indica la creación RRA:AVERAGE:0.5:1:60, queremos utilizar 60 puntos de control tomados cada 60 segundos. Este es bastante fácil, veamos el siguiente.

RRA:MIN:0.5:1:360

Ahora estamos guardando 360 puntos generados cada 60 segundos, es decir, cubrimos 60×360=21600 segundos=6 horas. Era sencillo.

RRA:AVERAGE:0.5:5:144

En la siguiente gráfica queremos obtener datos de las últimas doce horas (12 horas x 60 minutos x 60 segundos, 43200 segundos). Ahora ya no necesitamos saber el comportamiento exacto en cada momento sino que necesitamos una visión global de lo que ha ocurrido durante esas doce horas, tomamos, por tanto, un punto de control cada cinco datos de registro, es decir, estamos manteniendo un dato cada 5×60 segundos, 300 segundos. Necesitaremos por tanto 43200/300=144 valores para obtener nuestra gráfica de las últimas 12 horas

RRA:MIN:0.5:10:202

La última gráfica es semanal, 7x24x60x60=604800 segundos. Acepto que con tener un valor cada 10 minutos será suficiente, tendré entonces un valor cada 10×300=3000 segundos. 604800/3000=~202.

Es algo difícil de entender al principio, pero una vez lo piensas, todo tiene sentido.

Guardando las estadísticas

Tenemos ya nuestra base de datos preparada, creemos ahora la tarea que guardará en este archivo rrd los datos estadísticos. En mi caso es una tarea PHP que lo hace todo muy sencillo.

tarea.php

$url="http://dominio.com/?stats";
$cont=file_get_contents($url);
system("rrdtool update estadisticas.rrd N:".$cont);

Añadimos la tarea al cron para que se ejecute cada 60 seguntos.

*/1 * * * * /usr/bin/php /path/to/tarea.php > /dev/null 2>&1

¡Y ya está! Nuestra base de datos de estadísticas se está alimentando automáticamente.

Creando las gráficas

Lo más importante a fin de cuentas, lo que realmente vamos a utilizar. Rrdtool viene con un script de ejemplo que debes colocar en el directorio cgi-bin de tu servidor web y realizar los ajustes adecuados. Yo he puesto como ejemplo la gráfica de la última hora, las demás se harían igual variando el parámetro “–start” a los segundos anteriores que queremos mostrar.

/cgi-bin/stats.cgi

#!/usr/bin/rrdcgi

Tu Dominio - Home Stats

<RRD::GRAPH /usr1/www/www.genteirc.com/htdocs/imagenes/daily-visitas.png --imginfo '<img src="http://tudominio.com/%s" alt="" width="%lu" height="%lu" />' --start -3600 --end -0 -h 400 -w 600 --vertical-label="Tiempo" --title="Last Hour" -a PNG -l 0 DEF:ws1=/path/to/estadisticas.rrd:ws1:AVERAGE LINE1:ws1#00FF00:WebSer1t GPRINT:ws1:AVERAGE:"Media : %.4lft" GPRINT:ws1:MAX:"Max : %.4lft" GPRINT:ws1:MIN:"Min : %.4lfl" DEF:ws2=/path/to/estadisticas.rrd:ws2:AVERAGE LINE1:ws2#800000:WebSer2t GPRINT:ws2:AVERAGE:"Media : %.4lft" GPRINT:ws2:MAX:"Max : %.4lft" GPRINT:ws2:MIN:"Min : %.4lfl" DEF:ws3=/path/to/estadisticas.rrd:ws3:AVERAGE LINE1:ws3#FF8000:WebSer3t GPRINT:ws3:AVERAGE:"Media : %.4lft" GPRINT:ws3:MAX:"Max : %.4lft" GPRINT:ws3:MIN:"Min : %.4lfl" DEF:ws4=/path/to/estadisticas.rrd:ws4:AVERAGE LINE1:ws4#400080:WebSer4t GPRINT:ws4:AVERAGE:"Media : %.4lft" GPRINT:ws4:MAX:"Max : %.4lft" GPRINT:ws4:MIN:"Min : %.4lfl" DEF:ws5=/path/to/estadisticas.rrd:ws5:AVERAGE LINE1:ws5#0000FF:WebSer5t GPRINT:ws5:AVERAGE:"Media : %.4lft" GPRINT:ws5:MAX:"Max : %.4lft" GPRINT:ws5:MIN:"Min : %.4lfl" DEF:ws6=/path/to/estadisticas.rrd:ws6:AVERAGE LINE1:ws6a#00FFF0:WebSer6t GPRINT:ws6a:AVERAGE:"Media : %.4lft" GPRINT:ws6:MAX:"Max : %.4lft" GPRINT:ws6:MIN:"Min : %.4lfl" DEF:ws7=/path/to/estadisticas.rrd:ws7:AVERAGE LINE1:ws7#FF00FF:WebSer7t GPRINT:ws7:AVERAGE:"Media : %.4lft" GPRINT:ws7:MAX:"Max : %.4lft" GPRINT:ws7:MIN:"Min : %.4lfl" DEF:ws8=/path/to/estadisticas.rrd:ws8:AVERAGE LINE1:ws8#FFFF00:WebSer8t GPRINT:ws8:AVERAGE:"Media : %.4lft" GPRINT:ws8:MAX:"Max : %.4lft" GPRINT:ws8:MIN:"Min : %.4lfl" DEF:total=/path/to/estadisticas.rrd:total:AVERAGE LINE2:total#FF0000:'Total t' GPRINT:total:AVERAGE:"Media : %.4lft" GPRINT:total:MAX:"Max : %.4lft" GPRINT:total:MIN:"Min : %.4lfl" >

Para cada línea que queremos mostrar en la gráfica le indicamos de qué archivo rrd saldrán los datos y la fuente (DS) que vamos a utilizar. Añadimos además como leyenda los datos de media, máximo y mínimo de cada fuente, de manera que en una sola gráfica tenemos toda la información necesaria. Si ahora vas a la url donde has dejado el cgi tendrás:

http://tudominio.com/cgi-bin/stats.cgi

stats

Y tu te preguntarás, con lo pesado que te pones con Cacti, ¿por qué no meterla ahí también? Buena pregunta :P. Si tengo un ratito os enseño cómo integrarlo también con Cacti :).

Me he pasado con este artículo, no se lo va a leer nadie :P.

SCRUM de dos equipos en distintas ciudades

El pasado mes de mayo comenzamos el desarrollo de un nuevo proyecto que ha terminado recientemente, al menos la primera fase del mismo. Partimos casi de cero, los requerimientos eran muy básicos y poco documentados, pero parte del equipo teníamos en la cabeza exactamente lo que teníamos que hacer, de hecho era plasmar en una única aplicación todo nuestro trabajo de los últimos cuatro años.

Desde el principio nadie tuvo dudas, SCRUM era la mejor metodología posible para cumplir los plazos que nos habían impuesto. Planteamos sprints de dos semanas y dailys diarios (obviamente) a las 10 de la mañana de alrededor de 10 minutos. Como scrum master se quedó uno de nuestros project managers y como product owner otro del departamento de operaciones. La mayoría nos habíamos leído ya el Scrum desde las trincheras, pero una cosa es la teoría y otra muy diferente la práctica, y ahí casi nadie teníamos experiencia.

No trataré en este artículo de enseñaros SCRUM, no soy un experto, como mucho un poco “evangelizador” :P, simplemente trataré de explicar mis sensaciones tras cinco meses de SCRUM intensivo.

El Equipo

En el proyecto participaron dos equipos de desarrollo, uno en Valencia de 6 personas y otro en Madrid en el que llegaron a trabajar más de 30. Ninguna de estas casi 40 personas tenía disponibilidad completa para este proyecto sino que hubo que redistribuir toda la carga de trabajo para, con el mismo equipo, asumir un nuevo proyecto de cinco meses de duración. Salió bastante bien. Los dailys se hacían vía conferencia entre una sala de reuniones en Madrid y otra en Valencia. Se hicieron, además, infinidad de reuniones a dos bandas entre los dos equipos para decidir funcionalidades, discutir los puntos donde no había acuerdo y resolver dudas.  No sólo participaban desarrolladores sino también jefes de equipo, project managers, diseñadores, “expertos” en usabilidad, gente de testing y calidad, documentalistas, gente de sistemas, de datawarehouse

En la parte tecnológica tuvimos otro grave problema. Desde Madrid desarrollaban en .NET y desde Valencia en PHP. ¿Cual es el problema? :P. Se diseñó la arquitectura de manera que la aplicación trabajase indistintamente en uno u otro lenguaje. Dentro de un mismo entorno web, unos módulos se cargan de un lado y los otros del otro de forma completamente transparente al usuario. Se diseñó un sistema de single sign on que pudiesen compartir y consultar ambas tecnologías y, asociado a éste, todo un mecanismo de seguridad de acceso a distintos menús y opciones de la aplicación. El resultado fue formidable, todo funciona perfectamente de una manera integrada, robusta, eficiente y segura.

La Pila de Producto (product backlog)

Con la poca documentación que se tenía al principio se elaboró una pequeña pila de producto que fue creciendo a medida que las tareas de análisis evolucionaban. A esto debemos sumar requerimientos que iban llegando por parte de los departamentos comerciales y de operaciones. Al final el backlog era considerable. En agosto hubo que decidir quitar algunas historias de usuario ya que era imposible acometer todo lo que se quería, de hecho los requerimientos a estas alturas eran infinitamente superiores a los que se habían supuesto en mayo. Aún así, eliminando cosas, se hizo un producto mucho más ambicioso de lo esperado. Las historias de usuario eliminadas no se olvidaron, simplemente se trasladaron a una segunda versión.

Para el seguimiento del SCRUM, en vez de la clásica pizarra y dada la dispersión de los equipos, decidimos utilizar una herramienta de software. Comenzamos con un sencillo Excel y a mitad del proyecto incorporamos ScrumWorks, no creo que sea el programa ideal pero cumple su función.

Los Sprints

Los tres primeros sprints fueron prácticamente de análisis. Se hicieron documentos funcionales y orgánicos de todos los módulos requeridos y se perdió bastante tiempo definiendo la arquitectura de la aplicación, en realidad de las dos aplicaciones (.NET y PHP). Todo esto nos llevó a plantarnos a mediados de junio, tras 3 sprints, con muy poco que mostrar en la demo, y nos condujo a la consabida reprimenda de los que mandan, nos estábamos desviando (teóricamente) de la planificación estipulada. Sin embargo el tiempo nos daría la razón, el tiempo perdido en el diseño de la arquitectura se recuperó con creces en sprints siguientes y llegamos a la fecha con el proyecto terminado. Bueno, en realidad dejamos para el final un pequeño detalle, el rendimiento, que se solucionó en un sprint adicional.

A todo esto hemos de añadir, como ya he dicho, que el equipo no estaba dedicado al proyecto, cada día variaba la gente que entraba al daily ya que no todos estaban trabajando en el proyecto en ese momento. A eso hay que sumarle lo que llamo “contingencias del trabajo diario“, es decir, nuevos proyectos que van surgiendo durante todo ese tiempo y que implican dedicarle tiempo que tienes que quitarle a lo verdaderamente importante. Por si eso no fuera poco, a finales de junio se nos informa que una parte de la aplicación, un módulo de reporting, tiene que estar operativa a finales de julio puesto que se necesita para dar servicio en otros proyectos. Esto, que de palabra suena sencillo, nos obligó a modificar la planificación, el product backlog y la pila de sprint ya que el funcionamiento de este módulo implicaba a otros (login, seguridad, diseño…). Y aún así cumplimos todos los plazos, increíble. Eso sí, otros proyectos tuvo que decidirse entre descartarlos o aprobar una desviación de una o dos semanas en el proyecto del que hablamos, con lo que de igual manera fueron descartados :P.

Las Estimaciones

Uno de los mayores problemas es, sin duda, estimar las horas de las tareas. Comenzamos quedándonos cortísimos y terminamos quedándonos cortos también, creo que rara vez se hizo una estimación cercana a la realidad. Es muy complicado, sin duda. Las tareas de análisis se sobrestimaron en su mayoría y las de desarrollo (la mayoría) se quedaron cortas. Para planificar los primeros sprints utilizamos Poker-Scrum, pero acabamos haciéndolo directamente de viva voz ya que decidimos que no aportaba nada. En los primeros sprints hubo que arrastrar bastantes tareas de uno a otro porque se habían estimado muy a la baja, sin embargo hacia el final se recuperó el tiempo perdido y las estimaciones no eran tan malas.

Las Demos

Las demos al cliente (interno en nuestro caso) son el peor momento del sprint, no pocos días nos tocó quedarnos hasta altas horas de la noche para llegar a la demo del día siguiente con el trabajo terminado o, al menos, visible. Si esto lo hacíamos cada dos semanas, imaginaos que hubiese pasado si utilizásemos metodologías tradicionales, hubiésemos llegado al final del tiempo de desarrollo sin hacer ninguna demo, sin haber probado las cosas de una manera real. Habría sido imposible, de hecho es lo que ocurre habitualmente :P.

En nuestro caso nos sirvió no sólo para ver la reacción del cliente ante el avance del producto sino también para ver las fortalezas y debilidades del trabajo que estábamos desarrollando y reorientar los siguientes sprints.

Las demos están para que el cliente vea el avance del producto que se le está desarrollando. El cliente en nuestro caso era una persona en representación de uno o varios departamentos internos de la organización. ¿Qué utilidad tienen las demos si sólo asiste el product owner? Digo esto porque de vez en cuando aparecía por allí algún despistado que se dedicaba a criticar el producto, en concreto partes del mismo que llevaban dos o tres meses terminadas y validadas por el product owner. ¿Por qué esa actitud revienta-demos? O mejor aún, ¿por qué no le prestas el debido interés a un producto en el que se basará todo tu trabajo los próximos años? Ah, ya, es más fácil dejar la responsabilidad al product owner y después quejarse. Sin la implicación de toda la organización da igual qué metodología se utilice, ninguna conseguirá triunfar. Estamos hablando de media hora cada quince días, sólo eso.  Otro punto importante a tener en cuenta es la opinión. La gente que asiste a la demo debería estar obligada a decir algo y no quedarse callados como si no fuera con ellos porque al final ocurre lo mismo, cuando tienen que opinar no opinan y después, cuando ya no se puede hacer nada, es cuando hablan.

Retrospectiva y planificación

Las reuniones de retrospectiva fueron bastante interesantes. Por un lado comentábamos la indiferencia de los asistentes a las demos y por otro discutíamos sobre los problemas que se habían presentado durante el sprint, unas veces con más energía y otras con menos. En realidad casi podríamos decir que servían como válvula de escape al pasotismo de los asistentes a la demo.  Son necesarias las retrospectivas, sirven precisamente para evaluar no sólo lo que ha ocurrido sino también los ánimos del equipo.

Tras la retrospectiva llega la reunión de planificación de sprint. Tal como indicaba más arriba, las estimaciones se hicieron muy complicadas, muchísimas tareas por historia de usuario, muchísimas dependencias entre ellas y no siempre tenemos todos en la cabeza las implicaciones que pueden tener unas con otras, lo que termina en estimaciones muy poco aproximadas por no decir aleatorias. Aún así poco a poco se fueron afinando bastante. En las reuniones de planificación se ponían sobre la mesa también nuevos requerimientos que habían ido surgiendo y modificaciones sobre los ya terminados que obligaban a modificar las prioridades de la pila de producto ajustando el calendario a las nuevas necesidades y objetivos hasta llegar a la pila del sprint siguiente.

Conclusiones

Sin duda la experiencia ha sido muy positiva. En otras ocasiones habíamos hecho intentos de aplicar SCRUM a determinados proyectos  que resultaron en utilizar solamente algunos conceptos de SCRUM, pero la falta de implicación del cliente (casi siempre interno) restaba utilidad a la metodología. En esta ocasión se hizo una aplicación completa, real y efectiva, implicando a todos los elementos de la organización que tuviesen algo que decir dentro del proyecto y, aunque siempre se pueden hacer críticas a la implicación de algunos, podemos afirmar que, en general, todos cumplieron con su parte.

Una de las cosas de SCRUM que todos alabamos cinco meses después son, sin duda, los dailys. Nos sirvieron a todos los partícipes del proyecto para conocer de buena mano qué estaban haciendo los demás, qué problemas había,  cuándo se iban a resolver, etc. Creo que cualquier equipo de desarrollo, independientemente de la metodología que utilice, debería realizar reuniones diarias para compartir ideas y problemas.

La idea de tener demos del producto cada dos semanas obliga a todo el equipo a pensar más en hacer cosas que funcionen que en ir avanzando en tareas, es más importante terminar dos historias de usuario para presentar al final del sprint que comenzar 20 tareas que no se terminen.

Uno de los puntos que nos desviaron del objetivo en cada sprint fue el proceso de integración y preparación para la demo. Nunca tuvimos en cuenta estas horas que al final implicaban bastante tiempo de todo el equipo ya que al realizar la integración siempre había cosas que no funcionaban bien. El último día estaba dedicado casi íntegramente a estas labores que nunca estaban planificadas en la pila del sprint.

Los que me conocen saben que llevo ya unos años defendiendo SCRUM como metodología adecuada para el desarrollo de software. Esta experiencia no ha hecho más que confirmar esta opinión, la adaptación del producto al cliente que se consigue no se podría lograr con metodologías clásicas en las que la más mínima modificación de las tareas a realizar puede provocar una desviación enorme.

Supongo que pagamos algo cara la falta de experiencia, sobre todo al principio, pero aún así logramos el objetivo, seguramente con un poco más de experiencia el resultado habría sido aún mejor.

Por cierto, no hemos dejado SCRUM, el proyecto sigue y ya trabajamos en la siguiente release, así que seguimos liados con dailys, sprints, estimaciones…

Sobre diseñadores, programadores y formación

El pasado sábado un buen amigo me pidió que le acompañase a un seminario sobre Flex. No me apetecía mucho, y menos aún madrugar un fin de semana, pero es lo que tiene la amistad. Al final resultó que era un pequeño avance de lo que sería un curso de formación continua de 30 horas sobre desarrollo de RIA con Flex3. El profesor era programador profesional y bastante majete, pero mi sorpresa llegó cuando preguntó a la sala por el perfil profesional que teníamos. Para un curso de desarrollo había aproximadamente 28 diseñadores sobre 30 personas. Mi cara de asombro fue abrumadora, alguien no había entendido de qué iba la movida. Mi amigo, perro viejo ya en estas lides, no se cortó un pelo y comenzó a preguntar sobre las salidas de una herramienta como Flex para un diseñador, y ahí llegó lo peor, el profesor hizo una defensa a ultranza de las posibilidades que tenía, obviamente siempre bajo su punto de vista de programador, para muestra un botón:

Es muy sencillo, no tienes que saber programación, casi todo se hace con una línea de código. Yo tardé muy poco en comenzar a trabajar, aunque es verdad que tenía experiencia programando en muchos otros lenguajes.

De verdad, yo estaba alucinando. Lo peor es que convenció a mucha gente, y no lo digo por él, que me parece fenomenal que se gane un dinero, lo digo por la utilidad de un curso así para un diseñador. Tampoco debemos olvidarnos del otro lado de la ecuación. Con lo fácil que es hoy en día informarte y documentarte sobre una herramienta, la gente acude sin saber absolutamente nada acerca de lo que es Flex y para qué sirve.

En mi humilde opinión hubiese mejorado considerablemente el curso si hubiese optado por cambiar la orientación hacia skining puro y duro en Flex, una de las tareas más complicadas (los programadores tampoco son diseñadores) y a la vez desconocidas y con más salidas (bajo mi punto de vista), pero claro, él tampoco tenía los conocimientos adecuados para dar un curso de diseño, así son las cosas. En varios momentos de la charla hizo referencia a Adobe Flash Catalyst, la nueva gran herramienta de Adobe que llevará a desarrollar RIA’s de próxima generación, la simbiosis perfecta entre diseño y programación, pero se limitó a decir cosas como: “según la demo que nos hicieron en junio“, “por lo poco que he visto“… ¡Podías haberla probado tu mismo! Y es que os aseguro que será una revolución, una integración absoluta entre apariencia y funcionalidad.

Y todo esto me lleva al meollo de la cuestión: la gran mentira de la formación (de cualquier tipo) en este país. A principios de esta década impartí varios cursos, se pagaban muy bien y era una muy buena oportunidad, pero mi ética me obligó a abandonar la formación. Os pondré un par de ejemplos:

  • Allá por 2000 impartí un curso de 20 horas sobre Windows NT (¿quien se acuerda de eso? 😛 ) para trabajadores de Xerox. Patético. A los asistentes lo único que les interesaba era saber configurar una impresora en una red local, el resto les daba absolutamente igual. Claro, imaginaos el estado anímico de la  gente cuando lo que les interesa de verdad no llega casi hasta el final… Da igual que necesites saber qué son privilegios de administrador o colas de impresión, ellos sólo querían que les explicase lo que necesitaban para su trabajo diario, lógico también.
  • Un año después impartí la parte práctica de un curso sobre comercio electrónico organizado por la Universidad Politécnica de Valencia.  Mi clase versaba sobre el desarrollo de una tienda electrónica (bases de datos, programación, pasarela de pagos…). ¿La audiencia? Estudiantes de últimos cursos de derecho, empresariales, psicología… ya se sabe, créditos de libre elección. ¿A quien se le ocurrió incluir algo tan técnico en el programa de este curso?

Este último ejemplo me llevó a mi primer desengaño con la formación. Las negociaciones las llevé a cabo con una de las cabezas más visibles de la Universidad Politécnica (no diré nombres 😛 ). Un día, comiendo juntos, este personaje me soltó tan libremente:

Tú, si quieres hacer un curso, no tienes más que decírmelo. Preparas un programa, me dices cuánto te quieres llevar, vemos cual sería el coste por alumno y del resto nos encargamos nosotros. Al ser cursos patrocinados por la Universidad siempre hay gente dispuesta a apuntarse y pagar lo que haga falta.

¡Qué inocente era yo de aquélla! Podía haberme forrado pero preferí dejarlo para quien no tuviese problemas morales, no entendía que en un curso medianamente técnico se mezclase gente de perfiles tan dispares llevando la utilidad del mismo al mero canje de créditos o a un diploma con el que rellenar un curriculum.

En aquellos días llegó mi divorcio definitivo con la formación. Acudí a una entrevista en una de esas empresas líderes en Valencia, de esas que se dedican a impartir cursos para certificaciones. Querían organizar un curso sobre desarrollo en C bajo Linux. Os recuerdo que hablamos de 2001, así que me pareció alucinante que a alguien se le ocurriese montar algo de ese calado, si aún hoy es raro ver un curso así imaginaos hace ocho años. Mi conversación terminó casi en discusión, a modo de resumen:

  • Yo:  ¿A qué perfil de alumnos va orientado el curso?
  • NI: A cualquiera que se apunte y pague.
  • YO: No, a ver, no puedes mezclar en un mismo curso a gente que no ha visto Linux en su vida, gente que no sabe programar, gente que ya programa en C bajo Windows y quiere cambiar, gente que tiene conocimientos básicos de programación…
  • NI: ¿Cómo que no? Con que paguen…
  • YO: Pero vamos a ver, ¿quieres que la gente aprenda algo y aprovechen el curso?
  • NI: (silencio…)

Ahí entendí que esto no estaba hecho para mi. No podía engañar así a la gente.
Y esto me lleva de vuelta al comienzo del artículo. De nuevo por aquella época (2001) este amigo mío, en sus ansias por mejorar como diseñador, se planteó tener unos conocimientos mínimos de programación que le permitiesen hacer cosas básicas por su cuenta, así que se me apunta en este último sitio del que os he hablado a un curso sobre Visual Basic 6. Sabéis cómo acabó la historia ¿no? Creo que no llegó ni a asistir una semana.

Esto es una mi@3%6, estoy yo que no tengo ni idea, están los que vienen a clase a plantear problemas que tienen en su trabajo y ver como resolverlos (ya programadores), están los que les da todo igual… imposible aprender nada. Si explica a los que saben, nosotros no nos enteramos de nada, y si nos explica a nosotros los demás se aburren.

Esto, queridos lectores, es la formación (al menos la técnica): dinero, subvenciones y más dinero. Ah, y adelgazar las cifras del paro, no nos olvidemos 😉 . Ah, y lo más triste de todo, no ha cambiado nada en los últimos años, incluso diría que vamos a peor.

Adobe AIR X – Obtener el número de versión de una aplicación durante la ejecución

Parece simple pero, pese a que hay métodos para casi todo en Adobe AIR, no hay manera de saber en tiempo de ejecución el número de versión de una aplicación. ¿Para qué puedes necesitarlo? Pues por ejemplo, como es mi caso, para mostrar la típica pantallita “Acerca de…” donde indiques automáticamente el número de versión. Me diréis, bueno, si, pero puedes tener una variable que actualices con cada cambio de versión. Claro, pero entonces tendría que acordarme de actualizarlo en tres puntos: esta nueva variable, el descriptor de la aplicación y el archivo XML de autoactualización. Si puedo eliminar uno de ellos ¿por qué no hacerlo?

[Bindable]
private var airApplicationVersion:String = "";

private function getVersion():void{
 var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
 var air:Namespace = appXML.namespaceDeclarations()[0];
 this.airApplicationVersion = appXML.air::version;
}

Así de fácil tendremos una variable “bindeable” para mostar el número de versión, por ejemplo:

<mx:Text text="Versión {airApplicationVersion}"/>

Espero que os sea útil 🙂 .