Weblogs Código

Blog Bitix

Lookahead y lookbehind en expresiones regulares con Java

febrero 17, 2019 09:00

Java

Las expresiones regulares son un gran invento y muy útil para comprobar que una cadena cumple el patrón definido en la expresión regular, hay coincidencias en partes de la cadena y para reemplazar coincidencias. Son muy potentes para realizar estas tareas pero al mismo tiempo pueden volverse en cierta medida complicadas.

Una de las funcionalidades que soportan las cadenas es búsqueda hacia delante o lookahead y búsqueda hacia atrás o lookbehind. La primera permite examinar los siguientes caracteres de la cadena analizada y la segunda los caracteres pasados.

Hay diferentes formas de lookahead, en Java la construcción que permite hacer lookahead es (?=X) donde X es la expresión siguiente, se puede negar la expresión en el caso de querer que no se cumpla X con (?!X). También existe lookbehind con la construcción (?<=X) para negar que no se cumpla X se ha de emplear (?<!X), como su nombre sugiere en vez de mirar hacia adelante mira hacia atrás en los caracteres ya analizados.

Una aplicación práctica en la que usar lookahead es para ocultar los números de una tarjeta de crédito, una cuenta bancaria o un bearer token de un API REST excepto los cuatro últimos caracteres, este podría ser el caso de que esta información sea incluida en los archivos de log de una aplicación que por seguridad es recomendable ocultar. En el artículo Ofuscar datos sensibles en las trazas con Log4j comento varias formas de hacerlo.

Una tarjeta de crédito está formada por 4 grupos de 4 dígitos separados por espacios cumpliendo la expresión regular \d{4} \d{4} \d{4} \d{4} y un bearer token puede seguir la expresión regular Bearer \w+. Para ocultar la información de estas cadenas excepto los cuatro últimos caracteres hay que comprobar que los primeros complen el patrón añadiéndolos en un grupo de captura para su reemplazo posterior y mirar los cuatro siguientes si también lo cumplen fuera del grupo de captura. En el caso de la tarjeta de crédito se mira que la expresión cumple los primeros números de una tarjeta de crédito y le siguen los restantes, la primera parte se incluye en un grupo de captura con los paréntesis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.log4j;
...
public class Main {
private static final Logger logger = LogManager.getLogger(Main.class);
public static void main(String[] args) {
...
logger.info(new SecuredMessage("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A", Arrays.asList("(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})", "(\\d{6})(?=\\d{2}[A-Z])")));
logger.info("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A");
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package io.github.picodotdev.blogbitix.log4j;
import org.apache.logging.log4j.message.Message;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SecuredMessage implements Message {
private static final int UNMASKED_CHARACTERS = 3;
private Message message;
private String string;
private Pattern pattern;
public SecuredMessage(Message message, Collection<String> patterns) {
this.message = message;
this.pattern = compilePatterns(patterns);
}
public SecuredMessage(String string, Collection<String> patterns) {
this.string = string;
this.pattern = compilePatterns(patterns);
}
...
@Override
public String getFormattedMessage() {
return securedMessage();
}
private String securedMessage() {
if (message != null) {
return securedMessage(message);
} else if (string != null) {
return securedString(string);
}
return "";
}
private Pattern compilePatterns(Collection<String> patterns) {
return Pattern.compile(patterns.stream().map(it -> "(" + it + ")").collect(Collectors.joining("|")));
}
private String securedMessage(Message message) {
return securedString(message.getFormattedMessage());
}
private String securedString(String string) {
String result = string;
Matcher matcher = pattern.matcher(string);
while (matcher.find()) {
String match = matcher.group();
String mask = mask(match);
result = result.replaceFirst(match, mask);
}
return result;
}
private String mask(String string) {
return string.replaceAll(".", "*");
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
configuration:
 status: warn

 appenders:
 console:
 name: STDOUT
 patternLayout:
 pattern: "%d{DEFAULT} %X{uuid} %-5level %60.60logger %msg%n"
 replace:
 regex: "(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})|(\\d{6})(?=\\d{2}[A-Z])"
 replacement: "**********"

 loggers:
 root:
 level: info
 appenderRef:
 ref: STDOUT

El resultado es el siguiente:

1
2
3
...
2019-02-10 11:22:47,652 INFO io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: ****************111, DNI: ******11A
2019-02-10 11:22:47,653 INFO io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: **********111, DNI: **********11A

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

» Leer más, comentarios, etc...

Blog Bitix

Las clases anidadas inner, anónimas y locales en Java

febrero 15, 2019 05:00

Java

Por regla general en Java cada clase se define en su propio archivo de código fuente, pdor ejemplo, una clase de nombre Order se ha de definir en el archivo Order.java. Sin embargo, esta no es la única forma de definir clases, es posible definir clases inner y anónimas que evita tener que crear un nuevo archivo de código fuente.

Las clases inner se definen dentro de otra clase cuando esa clase inner tiene alta cohesión (su lógica está muy relacionada con la clase que la contiene), en algunos casos se emplean para ocultar los tipos que implementan la lógica. Dependiendo de si la clase inner debe acceder a los datos de la clase que la contiene o no la clase inner se define como no estática o como estática con la palabra reservada static. Las clases inner estáticas no necesitan una referencia a la clase que la contiene y por ello son algo más eficientes y el método preferido de definirlas, si la clase inner debe acceder a los miembros de la clase contenedora hay que definirla como no estática. Para desambiguar la referencia this y miembros con el mismo nombre de la clase inner con la de la clase contenedora se puede utilizar en el ejemplo Order.this.products quitando los static de las clases.

Las clases anónimas pueden definirse en la misma línea de código donde se declara su referencia, se denominan anónimas porque no se les asigna un nombre como en el ejemplo es el caso de la clase calculadora de precio para el descuento del más barato gratis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.github.picodotdev.blogbitix.javainnerclasses;
import java.math.BigDecimal;
public class Product implements Comparable<Product> {
private BigDecimal price;
public Product(BigDecimal price) {
this.price = price;
}
public BigDecimal getPrice() {
return price;
}
@Override
public int compareTo(Product o) {
return price.compareTo(o.getPrice());
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package io.github.picodotdev.blogbitix.javainnerclasses;
import java.util.Collection;
import java.math.BigDecimal;
import java.util.stream.Collectors;
public class Order {
Collection<Product> products;
// Inner
 public enum Discount {
NORMAL, DISCOUNT_10, CHEAPEST_FREE
}
public Order(Collection<Product> products) {
this.products = products;
}
public BigDecimal calculatePrice(Discount discount) {
return new PriceCalculatorFactory().getInstance(discount).calculate(products);
}
// Inner static
 private static class PriceCalculatorFactory {
PriceCalculator getInstance(Discount discount) {
switch (discount) {
case DISCOUNT_10:
return new DiscountCalculator(new BigDecimal("0.90"));
case CHEAPEST_FREE:
// Anonymous
 return new NormalCalculator() {
@Override
BigDecimal calculate(Collection<Product> products) {
Collection<Product> paid = products.stream().sorted().skip(1).collect(Collectors.toList());
return super.calculate(paid);
}
};
case NORMAL:
 default:
return new NormalCalculator();
}
}
}
// Inner static
 private static abstract class PriceCalculator {
abstract BigDecimal calculate(Collection<Product> products);
}
// Inner static
 private static class NormalCalculator extends PriceCalculator {
@Override
BigDecimal calculate(Collection<Product> products) {
return products.stream().map(i -> i.getPrice()).reduce(new BigDecimal("0.00"), (a, b) -> { return a.add(b); });
}
}
// Inner static
 private static class DiscountCalculator extends PriceCalculator {
private BigDecimal discount;
public DiscountCalculator(BigDecimal discount) {
this.discount = discount;
}
@Override
BigDecimal calculate(Collection<Product> products) {
PriceCalculator calculator = new NormalCalculator();
return calculator.calculate(products).multiply(discount);
}
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.github.picodotdev.blogbitix.javainnerclasses;
import java.util.Collection;
import java.util.ArrayList;
import java.math.BigDecimal;
import java.text.DecimalFormat;
public class Main {
public static void main(String[] args) {
Collection<Product> products = new ArrayList<>();
products.add(new Product(new BigDecimal("5.0")));
products.add(new Product(new BigDecimal("10.0")));
products.add(new Product(new BigDecimal("15.0")));
Order order = new Order(products);
DecimalFormat df = new DecimalFormat("#,###.00");
System.out.printf("Price (normal): %s%n", df.format(order.calculatePrice(Order.Discount.NORMAL)));
System.out.printf("Price (discount 10%%): %s%n", df.format(order.calculatePrice(Order.Discount.DISCOUNT_10)));
System.out.printf("Price (chapest free): %s%n", df.format(order.calculatePrice(Order.Discount.CHEAPEST_FREE)));
}
}
1
2
3
Price (normal): 30,00
Price (discount 10%): 27,00
Price (chapest free): 25,00

Para los programadores en Java seguramente esto de las clases inner y anónimas no es nada nuevo pero ¿conoces las clases locales? Dentro de un método también se puede definir una clase, llamada por ello local. Las clases locales no son habituales y para su uso su funcionalidad ha de estar altamente cohesionado con el método, un posible uso es para realizar validaciones o formateos que sean un poco complejos. El siguiente ejemplo de clase local PhoneNumber muestra su uso.

En la sección de clases anidadas o nested classes del tutorial sobre clases y objetos se explica más detalladamente estas capacidades del lenguaje Java.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

» Leer más, comentarios, etc...

Variable not found

Registro y obtención de múltiples implementaciones de servicios en ASP.NET Core, y un caso práctico

febrero 13, 2019 05:47

ASP.NET CoreComo sabemos, ASP.NET Core incluye un sistema de inyección de dependencias que, aunque es algo simple comparado con otras alternativas más veteranas, cubre la mayoría de necesidades habituales. Por ejemplo, un aspecto que no es muy conocido y que puede ser útil en muchas ocasiones es su capacidad para registrar y recuperar múltiples implementaciones de una misma interfaz.

En este post vamos a ver cómo conseguirlo, y un caso práctico de uso de esta técnica en un escenario muy frecuente.

Registro de múltiples implementaciones de la misma interfaz

Como se puede intuir, el registro de distintas implementaciones de la misma interfaz se realiza… pues eso, registrándolas ;) No hay ningún misterio aquí, simplemente añadimos el par <TService, TImplementation> en la clase Startup de ASP.NET Core, usando el lifetime apropiado (singleton, en el ejemplo de abajo):
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
... // Add more IImageFileProcessor implementations
}
...
}

Obtención de múltiples implementaciones registradas

Aquí viene la parte que es menos intuitiva, aunque tampoco anda muy lejos de lo que podríamos esperar del sistema de inyección de dependencias de ASP.NET Core. Para reclamar al contenedor las instancias de tipos asociados a la interfaz TService, simplemente añadimos un parámetro de tipo IEnumerable<TService> en el constructor:
public class ImageController: Controller
{
private readonly IEnumerable<IImageFileProcessor> _imageProcessors;

public ImageController(IEnumerable<IImageFileProcessor> imageProcessors)
{
_imageProcessors = imageProcessors;
}
...
}
Esta colección de instancias de TService que ya tenemos en memoria podemos utilizarla para implementar las funcionalidades que necesitemos para nuestra aplicación.

Pero mejor, veámoslo todo sobre un caso práctico…

Un caso práctico: SOLIDificar código

Imaginad un código como el siguiente, donde se muestra un método Process() que lo único que hace es llamar a otros métodos que procesan objetos de tipo ImageFile bajo determinadas circunstancias:
public class ImageFileProcessor
{
public ImageFile Process(ImageFile imageFile)
{
if (imageFile.Type == "JPG")
imageFile = ProcessJpeg(imageFile);

if (imageFile.Type == "PNG")
imageFile = ProcessPng(imageFile);

if (imageFile.Size > 100_000)
imageFile = ProcessHeavyFile(imageFile);

if (imageFile.Width > 1024 || imageFile.Height > 768)
imageFile = ProcessBigFile(imageFile);

[...] // Call to another processors when needed

return f;
}

private ImageFile ProcessJpeg(ImageFile f) { ... }
private ImageFile ProcessPng(ImageFile f) { ... }
private ImageFile ProcessHeavyFile(ImageFile f) { ... }
private ImageFile ProcessBigFile(ImageFile f) { .. }
[...] // Other processors
}
Entre otros, este código atenta directamente contra Principio de Responsabilidad Única, dando lugar a una clase que a todas luces tendrá a ser muy extensa y difícilmente mantenible al tener demasiadas responsabilidades. Nada que no podamos romper usando algo de Estrategia ;)

El primer paso para mejorar esta implementación sería detectar que en nuestra aplicación existen procesadores de archivos de imagen que se aplican en función de determinados criterios. Con esta información podríamos crear la siguiente abstracción:
public interface IImageFileProcessor
{
bool CanProcess(ImageFile imageFile);
ImageFile Process(ImageFile imageFile);
}
En nuestra aplicación existirán tantas implementaciones de IImageProcessor como formas de procesar los ficheros de imagen, y cada uno de estos componentes incluirá tanto la lógica para decidir si puede procesarlas como el proceso en sí mismo:
public class JpegFileProcessor : IImageFileProcessor
{
public bool CanProcess(ImageFile imageFile) => imageFile.Type == "JPG";

public ImageFile Process(ImageFile imageFile)
{
// Process here the JPEG file
}
}

public class HeavyFileProcessor : IImageFileProcessor
{
public bool CanProcess(ImageFile imageFile) => imageFile.Size > 100_000;

public ImageFile Process(ImageFile imageFile)
{
// Process here the heavy file
}
}

...
De momento, esta estructura ya nos permitiría simplificar la clase inicial, liberándola de responsabilidades que no le corresponden. Serán los componentes procesadores, las distintas implementaciones de IImageFileProcessor, los que llevarán ese peso.

Aplicando la técnica que hemos visto anteriormente, podríamos simplificar la clase inicial ImageFileProcessor suministrándole mediante inyección de dependencias la colección de procesadores, y haciendo que su método Process() simplemente los recorra y ejecute de forma secuencial:
public class ImageFileProcessor
{
private readonly IEnumerable<IImageFileProcessor> _imageProcessors;

public ImageFileProcessor(IEnumerable<IImageFileProcessor> imageProcessors)
{
_imageProcessors = imageProcessors;
}

public ImageFile Process(ImageFile imageFile)
{
foreach (var processor in _imageProcessors)
{
if (processor.CanProcess(imageFile))
{
imageFile = processor.Process(imageFile);
}
}
return imageFile;
}
}
Para que el componente ImageFileProcessor esté disponible en todos los puntos de nuestra aplicación deberíamos registrarlo en el contenedor de dependencias. Asimismo, ahí registraríamos todos los procesadores de imágenes que hayamos implementado:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<ImageFileProcessor>();
services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
... // Add more IImageFileProcessor implementations
}
...
}
Ojo, aunque en este caso utilizamos un registro de tipo singleton, esto puede variar en cada escenario en función de las necesidades.
Fijaos que si quisiéramos añadir un nuevo procesador de imágenes, por ejemplo un GifProcessor no sería necesario tocar la clase ImageFileProcessor, ni ninguno de los procesadores existentes. Simplemente crearíamos la clase correspondiente implementando IImageFileProcessor y la registraríamos en el contenedor… and that’s all, folks!
...
services.AddSingleton<IImageFileProcessor, GifFileProcessor>();
En resumen, en este post hemos visto cómo utilizar el sistema de inyección de dependencias de ASP.NET Core para registrar múltiples implementaciones de la misma interfaz y cómo recuperarlas más adelante para implementar soluciones muy limpias a problemas frecuentes.

Espero que podáis aplicarlo a vuestros propios desarrollos ;)

Publicado en Variable not found.

» Leer más, comentarios, etc...

Mascando Bits

Configuración de entorno Python2 y Python3 para uso simultáneo en Debian

febrero 13, 2019 07:30

Si eres desarrollador de Python o usuario de su ecosistema, te habrás dado cuenta de la brecha existente entre ambos, significando una ruptura de compatibilidad el salto de Python2 a Python3.

Pese a que Python2 tiene fecha de caducidad todavía existen aplicaciones y librerías que bien porque no han sido migradas a Python3, o porque tienen dependencias con librerías que aún no se han migrado a Python3, siguen funcionado exclusivamente sobre Python2. Por eso se hace necesario mantener los dos interpretes de Python con su respectivo respectivo gestor de paquetes pip.

Quizás a la hora de abordar esta situación, la plataforma de Windows tenga la aproximación más razonable e interesante. En la instalación de Windows pueden convivir ambos intérpretes, siendo Python3 el que prevalece por defecto si ambos existen, quedando vinculadas las palabras python y pip al intérprete y gestor de paquetes de Python3 respectivamente.

Si queremos diferenciar la ejecución entre ambos, tenemos py2 para referirnos al intérprete de Python2 y py3 para el de Python3. De la misma manera, si queremos referirnos al gestor de paquetes de Python2 lo haremos con pip2 y para referirnos al de Python3 lo haremos con pip3.

Esta estupenda idea no se da en sistemas Debian y derivados como Ubuntu. A pesar de todo Debian 8 (Jessie) trae Python 3.4 y Debian 9 (Stretch) Python 3.5, en ambos casos junto con Python 2.7, siendo Python 2.7 el intérprete Python por defecto. Para tener ambos entornos vamos a realizar las una serie de configuraciones.

Para ellos vamos a crear los siguientes alias:

alias py2='/usr/bin/python2.7'
alias py3='/usr/bin/python3.4'  # Para Debian 8 Jessie
alias py3='/usr/bin/python3.5'  # Para Debian 9 Stretch

Con esto si ejecutamos py2 o py3 tendremos configurado ambos intérpretes para usarlos según nos convenga.

Ahora tenemos que instalar los gestores de paquetes pip para Python2 y Python3, para ello ejecutamos los siguientes comandos:

apt-get install python-pip  # python2
apt-get install python3-pip  # python3

Ahora generamos los alias:

alias pip2='/usr/bin/pip'  # python2
alias pip3='/usr/bin/pip3'  # python3

Si deseamos listar los alias registrar podemos hacerlo escribiendo “alias” o “alias -p“. Si deseamos eliminar un alias sería “unalias nombre_del_alias” o “unalias -a” si queremos borrar todos.

Hasta aquí ya tenemos nuestros dos entornos para funcionar simultáneamente, pero cuidado:

⚠ Los alias se pierden al cerrar la sesión

Para arreglarlo sólo tenemos que poner los alias que queramos tener en la sesión en el archivo ~/.bash_aliases:

alias py2='/usr/bin/python2.7'
alias py3='/usr/bin/python3.5'  # Para Debian 8 Jessie
alias py3='/usr/bin/python3.5'  # Para Debian 9 Stretch
alias pip2='/usr/bin/pip'
alias pip3='/usr/bin/pip3

Para que se carguen al iniciar sesión tenemos que asegurarnos que en el archivo ~/.bashrc existan la siguientes líneas:

if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi

En caso de no encontrarlas, puedes añadirlas al final del fichero. En el caso de estar usando el usuario root es muy probable que tengas que añadir las líneas.

Para abrir y editar los archivos puedes usar nano:

nano ~/.bashrc
nano ~/.bash_aliases

Llegados a este punto ya tenemos los entornos de Python2 y Python3 completamente configurados pudiendo elegir con qué ejecutar nuestras aplicaciones y pudiendo lanzar ejecuciones con ambos intérpretes de manera simultánea. 😉

 

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 349

febrero 11, 2019 07:30

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros


Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Blog Bitix

Ofuscar datos sensibles en las trazas con Log4j

febrero 10, 2019 09:30

Java

Los archivos de trazas o logs contienen información de lo que ha realizado la aplicación. Estos registros de información contienen los datos que el desarrollador considera de utilidad en caso de necesitar su consulta. Algunos datos son especialmente sensibles ya que su obtención permiten acceder a cuentas de usuario, obtener datos como tarjetas de crédito o cuentas bancarias, contraseñas o bearer tokens de peticiones HTTP que autorizan el acceso. Proteger las contraseñas hasheandolas aún con salt y cifrar información por motivos seguridad y privacidad es inútil si luego esta información está presente en los archivos de log en texto plano.

Log4j es una de las librerías más utilizadas para añadir la funcionalidad de las trazas en una aplicación Java. Proteger algunos datos sensibles se puede hacer de varias formas. Una de ellas es hacer que sea la aplicación la que se encargue de no emitir estos datos en las trazas u ofuscarla enmascarándola al toda o parte. Para este caso se pueden utilizar objetos Message que adaptan los objetos de la aplicación a los datos a emitir en las trazas pero requiere modificar en todos los puntos de la aplicación.

En el siguiente ejemplo se hace uso de lookahead como se detalla en la clase Pattern de Java para añadir la funcionalidad de que los últimos caracteres queden visibles y la clase SecuredMessage aplica expresiones regulares al mensaje, en caso de encontrar una coincidencia realiza la ofuscación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.log4j;
...
public class Main {
private static final Logger logger = LogManager.getLogger(Main.class);
public static void main(String[] args) {
...
logger.info(new SecuredMessage("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A", Arrays.asList("(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})", "(\\d{6})(?=\\d{2}[A-Z])")));
logger.info("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A");
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package io.github.picodotdev.blogbitix.log4j;
import org.apache.logging.log4j.message.Message;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SecuredMessage implements Message {
private static final int UNMASKED_CHARACTERS = 3;
private Message message;
private String string;
private Pattern pattern;
public SecuredMessage(Message message, Collection<String> patterns) {
this.message = message;
this.pattern = compilePatterns(patterns);
}
public SecuredMessage(String string, Collection<String> patterns) {
this.string = string;
this.pattern = compilePatterns(patterns);
}
...
@Override
public String getFormattedMessage() {
return securedMessage();
}
private String securedMessage() {
if (message != null) {
return securedMessage(message);
} else if (string != null) {
return securedString(string);
}
return "";
}
private Pattern compilePatterns(Collection<String> patterns) {
return Pattern.compile(patterns.stream().map(it -> "(" + it + ")").collect(Collectors.joining("|")));
}
private String securedMessage(Message message) {
return securedString(message.getFormattedMessage());
}
private String securedString(String string) {
String result = string;
Matcher matcher = pattern.matcher(string);
while (matcher.find()) {
String match = matcher.group();
String mask = mask(match);
result = result.replaceFirst(match, mask);
}
return result;
}
private String mask(String string) {
return string.replaceAll(".", "*");
}
}

Utilizar una clase que implemente la interfaz Message para realizar el reemplazo requiere modificar todos los puntos de la aplicación que emitan información sensible, para evitar posibles omisiones este aspecto de la aplicación se puede delegar en Log4j y ser aplicado de forma global.

Con los parámetros de configuración replace, regex y replacement el reemplazo los hace la clase PatterLayout utilizando una expresión similar regular que en el caso de SecuredMessage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
configuration:
 status: warn

 appenders:
 console:
 name: STDOUT
 patternLayout:
 pattern: "%d{DEFAULT} %X{uuid} %-5level %60.60logger %msg%n"
 replace:
 regex: "(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})|(\\d{6})(?=\\d{2}[A-Z])"
 replacement: "**********"

 loggers:
 root:
 level: info
 appenderRef:
 ref: STDOUT

En la salida del ejemplo la primera traza corresponde al uso de la clase SecurdMessage y la segunda al PatternLayout.

1
2
3
...
2019-02-10 11:22:47,652 INFO io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: ****************111, DNI: ******11A
2019-02-10 11:22:47,653 INFO io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: **********111, DNI: **********11A

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

» Leer más, comentarios, etc...

Blog Bitix

Conferencia BilboStack 2019

febrero 08, 2019 10:15

La octava edición de la BilboStack sigue fiel a su cita en el calendario a finales de enero como en anteriores ocasiones. Tampoco cambia el formato de cuatro presentaciones en dos tracks simultáneos y de ser únicamente de media jornada a la mañana par disfrutar a la tarde del networking, comida y de Bilbao para aquellos que así quieran y aprovechar el viaje si se viene de fuera. Tampoco cambia el recinto como en la edición anterior de el Palacio Euskalduna, con un aforo bastante amplio aún así las entradas han llegado a agotarse y no han quedado prácticamente sitios libres en la sala A3.

La novedad más relevante de este año es que por primera vez las entradas han tenido un precio muy módico que no llega a los 15€ por asistente que junto a los patrocinadores permitirá a la organización cubrir en parte algunos costes como viajes de los ponentes, alojamiento, comida, recinto, etc. Otra novedad es que en el descanso después de las dos primeras presentaciones ha habido café y de comer para acompañarlo junto a los stands de varios de los patrocinadores para hacer contactos en el networking.

Palacio Euskalduna. Fuente: @BilboStack
Patrocinadores, ubicación y paquete de bienvenida

La conferencia comienza con la presentación y la bienvenida de los organizadores junto con unas palabras de Xabier Otxandiano concejal de desarrollo económico del ayuntamiento de Bilbao comentando la transformación que ha realizado la ciudad en las últimas décadas, urbanística siendo representante el propio palacio Euskalduna de un entorno más industrial a otro más de servicios y la importancia de la tecnología con el potencial para convertirse en la nueva industria de la ciudad. Por este motivo la conferencia BilboStack es importante y lo apoyan de forma institucional haciendo hincapié que no es fácil organizar una conferencia con el poder de convocatoria de casi 700 personas un sábado a la mañana y a la que muchos acuden haciendo muchos kilómetros de viaje.

Presentación

La agenda comienza a las 9:00 de la mañana del sábado con una presentación y terminaba a las 14:00 aunque por el control de acceso de este año ha sido recomendable llegar un poco antes para evitar alguna pequeña aglomeración en los últimos minutos y encontrar y entrar en las salas con suficiente antelación. Llega el momento de decidir a qué presentación de los dos tracks asistir, dependiendo de los intereses de cada uno a veces no es fácil y uno quisiera haber asistido a las dos.

Hora Sala Barria
09:00-09:20 Presentación
09:30-10:20 Kubernetes is not a deployment tool: it's a platform por Jose Armesto
10:30-11:20 Come reza data por Inés Huertas
11:30-12:00 Networking + Café
12:00-12:50 Devops is not what you think por Eduardo Ferro
13:00-13:50 10 retos de la creación de chatbots y asistentes con NLP por Cristina Santamarina
> 14:00 Networking + pintxos y poteo
Hora Sala A3
09:00-09:20 Presentación
09:30-10:20 Web Components API: esto va en serio por Belén Albeza
10:30-11:20 Agile JavaScript por Ricardo Borillo
11:30-12:00 Networking + Café
12:00-12:50 UX para desarrolladores front y back por Virginia Aguirre
13:00-13:50 Viaje desde Arquitectura Hexagonal al Event Sourcing por Carlos Buenosvinos
> 14:00 Networking + pintxos y poteo

Cómo ocasiones anteriores hago un compendio de las ideas con las que me quedé de las presentaciones a las que asistí, seguro que me dejo cosas de las comentadas.

Web Components API: esto va en serio por Belén Albeza

Desde los inicios la web está formada por dos elementos el protocolo HTTP y los documentos HTML con el contenido. Con el paso del tiempo las páginas añadieron CSS y comportamiento con el lenguaje JavaScript. En gran medida las bases iniciales no han cambiado y una página de hace 20 años se verán igualmente en un navegador actual, al contrario que las aplicaciones nativas que pueden dejar de funcionar con actualizaciones de dispositivos móviles en un lustro.

Los Web Components son una especificación que están implementando la navegadores y es la alternativa estándar que cubre algunos aspectos de las populares librerías JavaScript como React y Vue. Estas aportan estructura al JavaScript y permiten crear componentes que por defecto loa navegadores no ofrecen como un calendario o menús.

Web Components permite crear etiquetas propias y ser usadas en los documentos HTML como si de cualquier otra etiqueta estándar se tratase. Los web componentes se componen de un nombre, atributos y los eventos que lanza. Con la API de los Web componentes se proporciona el HTML que genera un componente, el comportamiento con JavaScript y las clases CSS que le aplican.

Las especificación es de los Web Components son varias. Una de las cosas que aportan los web components es que el CSS de estos no entren en colisión con cualquier otro CSS de la página o de otros web componentes.

En las DevTools de Firefox se puede inspeccionar el shadow DOM del web componentes. En la documentación de MDN hay varias páginas que detallan los Web Components con ejemplos.

Tenía claro que quería acudir a esta presentación, era una en la que no tenía muchas dudas al elegir por quien la daba @ladybenko de la que sigo Twitter sus interesantes comentarios que hace, desarrolladora de Firefox, es el nivel que hay en los ponentes de la BilboStack. Comenzaba la mañana posponiendo la alarma del despertador varias veces pero solo por esta presentación ya ha merecido el levantarme para acudir a la BilboStack.

Web Components API

Agile JavaScript por Ricardo Borillo

En el State of JavaScript del 2018 se mencionan numerosas herramientas de JavaScript más populares del momento y otras nuevas que están surgiendo como alternativa.

Entre las que se mencionan, no son pocas, están npm, nvm, Node.js, Webpack, Babel, Parcel, Rollup, Eslint, Prettier, Flow, TypeScript, Reason.

Esta presentación junto con la anterior forman la representación de JavaScript que siempre tiene la BilboStack y es que muchos lo utilizamos en mayor o menor medida.

Agile JavaScript

Descanso

No saque fotos pero algunos patrocinadores dispusieron stands en los que hacerse con algunas pegatinas y bagatelas, de las empresas una oportunidad de conocerlas e iniciar algún contacto.

Networking + Café, photocall y hashtag

UX para desarrolladores front y back por Virginia Aguirre

A veces hay más atención puesta en la tecnología que en la experiencia de usuario y en estos casos ocurren ejemplos como el Nokia Ngage con su peculiar forma para hacer llamadas, aplicaciones con gran cantidad de barras de herramientas que ocupan gran parte del espacio vertical de la pantalla o el incómodo menú inicio de Windows 8 más adaptado a interfaces táctiles que a escritorio. La UX hace hincapié en las necesidades del usuario primero, las necesidades del negocio y finalmente las posibilidades técnicas, el orden es importante.

UX aplicado es que el usuario pueda ver como quedan los muebles antes de comprarlos, ante esta necesidad del usuario Ikea desarrolla una aplicación de realidad aumentada que permite ver con la cámara del móvil una representación del mueble en la pantalla con la imagen del salón captada por la cámara. Otro ejemplo de uso es encontrar la gasolinera más cercana aprovechando la geolocalización de los móviles, dado que el contexto es uno de estar conduciendo la aplicación no ha de ser interactiva como es el caso de mostrar un mapa del país en el que ver las gasolineras y buscar entre ellas la más cercana. Con la geolocalización la aplicación ya puede conocer la ubicación del usuario y mostrar la más cercana que será el caso de uso más habitual.

En UX hay múltiples factores usuario, sociales, culturales, contexto de uso (casa, coche, móvil, escritorio) y el producto. Hay que entender el problema para proporcionar una solución efectiva, la solución puede desarrollarse de forma iterativa. Obtener información de los usuarios puede hacerse con analítica web, del departamento de atención al cliente, de formularios de encuestas, de noticias, informes sectoriales o analizando que hace la competencia.

De las que he asistido esta y la del otro track era la presentación que podría haber asistido a cualquiera, en cualquier caso siempre se descubre algún detalle interesante, como programador en mi caso varios puntos interesantes.

UX para desarrolladores front y back

Viaje desde Arquitectura Hexagonal al Event Sourcing por Carlos Buenosvinos

Las arquitecturas pueden evolucionar en seis niveles.

  1. Spaghetti
  2. Framework
  3. Hexagonal
  4. Hexagonal + Domain Event
  5. CQRS
  6. Event Sourcing

Estar en el nivel 3 y 4 probablemente para muchas aplicaciones ya sea suficiente. En las 1 no hay estructura y con el paso del tiempo añadir nuevas características se hace más difícil y el código más difícil de mantener. La 2 añade estructura al código pero lo hace dependiente del framework en forma de acoplamiento. Con hexagonal se trata de independizar la lógica de negocio del código de infraestructura entendiendo por infraestructura la parte ajena al modelo como es el caso del sistema de persistencia en concreto que se utilice, para el negocio que sea una base de datos relacional o NoSQL es indiferente. Con una arquitectura hexagonal se separan aspectos, se independiza del framework y retrasan las decisiones de infraestructura.

Hay funcionalidades que no forman parte del núcleo del negocio. Estas funcionalidades se pueden realizar al reaccionar a esos eventos, registrados en elastic search o rabbit se obtienen métricas en tiempo real de lo que sucede en la aplicación. Al mostrar una página la cantidad de información puede generar unas decenas o cientos de consultas a la base de datos y a medida que se añaden funcionalidades e información la página irá más lenta. Cuando se modifica una entidad se dispara un evento que un listener escucha y se encarga de recuperar la información actualizada y gudarla transformada según las necesidades de lectura para que con una consulta se obtenga toda toda la información de la entidad, en este momento el rendimiento de la aplicación no se degrada al añadir nuevas características. En este sistema donde las consultas y modificaciones están separadas, con la serie de eventos que provocan cambios se puede reconstruir el estado final de una entidad aplicando la serie de eventos que sucedieron secuencialmente.

Viaje desde Arquitectura Hexagonal al Event Sourcing

» Leer más, comentarios, etc...

Blog Bitix

Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring

febrero 08, 2019 07:00

Spring
Java

Hace unos días encontré un articulo del blog técnido de los desarrolladores de Idealista. En él comentaban que tenían una API para realizar simulaciones hipotecarias usando Spring como framework, Spring Security OAuth como forma de autenticación y autorización y JWT como forma de codificar el token que otorga el servidor OAuth y contiene la información necesaria para que el servidor de recursos permita o no el acceso al recurso que aloja.

Ya había oído mencionar JWT pero este artículo me ha permitido conocer su utilidad, y no es poca. Como se menciona en el artículo JWT tiene la ventaja de que que no es necesario persistirlo en una base de datos y contiene toda la información que el servidor de recursos necesita para realizar la autorización ya que es capaz de cargar con información arbitraria que el servicio desee en el momento de la emisión, la autenticación y comprobación de que ha sido emitido por el servidor OAuth la realiza sabiendo que el token está firmado.

Los tokens son una serie de caracteres aparentemente sin sentido al estar hasheados y firmados con una clave compartida entre servidor OAuth y el servidor de recurso o para mayor seguridad mediante clave privada en el servidor OAuth y su clave pública asociada en el servidor de recursos, con la firma el servidor de recursos el capaz de comprobar la autenticidad del token sin necesidad de comunicarse con él. Los tokens de OAuth son más cortos, los tokens JWT con más largos ya que contienen información adicional. Se componen de tres partes separadas por un punto, una cabecera con el algoritmo hash utilizado y tipo de token, un documento JSON con datos y una firma de verificación.

El hecho de que los tokens JWT no sea necesario persistirlos en base de datos elimina la necesidad de tener su infraestructura, como desventaja es que no es tan fácil de revocar el acceso a un token JWT y por ello se les concede un tiempo de expiración corto. En el articulo se analizaba su infraestructura y hay varios elementos configurables de diferentes formas, son:

  • El servidor OAuth que proporciona los tokens, realiza la autenticación y proporciona las autorizaciones.
  • El servidor del recurso al que se le envía el token, en base a las autorizaciones otorgadas por el servidor OAuth al token y las autorizaciones necesarias para acceder al recurso concedo o no acceso al recurso.
  • En el caso de múltiples servicios con múltiples recursos es conveniente un gateway para que sea el punto de entrada de todos los servicios, de esta forma los clientes solo necesitarán conocer el gateway en vez de los múltiples servicios individuales. El gateway se encarga de hacer de proxy en base a información en la petición como ruta, host, parámetros, cabeceras, … de redirigir la petición al servicio encargado de atenderla y devolver la respuesta. Un ejemplo de gateway es Zuul como ya he mostrado en el artículo Proxy para microservicios con Spring Cloud Netflix y Zuul.

Puede haber más elementos en la infraestructura y quizá sea el caso de un sistema real como sería un servidor de descubrimiento con Eureka o un servidor de configuración con Spring Cloud Config, en la serie de artículos sobre Spring Cloud los muestro. Para este ejemplo obvio estos otros servidores y me centro en los más relacionados con el artículo. Aunque lógicamente son diferentes servicios se puede crear uno que proporcione varios de ellos al mismo tiempo, por ejemplo, un servicio que haga al mismo tiempo de servidor de OAuth y de gateway que es una de las posibles cambios que dejan al final en el artículo de Idealista.

Spring ha creado su propio proyecto de gateway para sustituir a Zuul, Spring Cloud Gateway y será el que use en este artículo. Soporta Spring Boot 2, Spring Framework 5, coincidencia por cualquier parámetro de la petición, filtros y transformaciones o predicados, el patrón circuit breaker, limitación de peticiones y reescritura de rutas.

Los servicios los mantengo separados ya que al combinarlos pueden surgir problemas de integración al usar diferentes versiones de librerías de Spring aún cuando todos los proyectos son de Spring. Por ejemplo, Spring Cloud Gateway utiliza Spring WebFlux que puede ser diferente del lo que utilice Spring Security OAuth y la integración puede no estar exenta de problemas.

OAuth JWT

Servidor OAuth

Empezando por el servidor OAuth y las dependencias que necesita, son spring-security-oauth2 y para generar tokens JWT spring-security-jwt, el resto son dependencias necesarias de Spring Boot

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
id 'application'
id 'org.springframework.boot' version '2.0.8.RELEASE'
}
mainClassName = 'io.github.picodotdev.blogbitix.springoauth.oauth.Main'
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')
def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-security', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE', excludeSpringBootStarterLogging)
compile('org.springframework.security:spring-security-jwt:1.0.10.RELEASE', excludeSpringBootStarterLogging)
runtime('com.google.code.gson:gson:2.8.5')
runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
runtime('javax.xml.bind:jaxb-api:2.3.0')
runtime('com.sun.xml.bind:jaxb-impl:2.3.0')
runtime('org.glassfish.jaxb:jaxb-runtime:2.3.0')
runtime('javax.activation:activation:1.1.1')
}

La clase principal de Spring Boot y que inicia la aplicación no tiene nada especial salvo la necesaria anotación @EnableAuthorizationServer para habilitar el servidor OAuth.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.github.picodotdev.blogbitix.springoauth.oauth;
...
@SpringBootApplication
@EnableAuthorizationServer
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}

La parte importante está en la clase de configuración. La clase JwtAccessTokenConverter se encarga de codificar el token, la clase TokenStore de generarlos, DefaultTokenServices contiene referencias a ambos, los métodos heredados configure() configuran diferentes aspectos del servicio como los requisitos para acceder a los endpoint para ver el contenido de un token o los clientes OAuth que reconoce. Para cada cliente se necesita proporcionar el identificativo del cliente, su clave privada o secret, identificativo del recurso, que tipos de concesiones, grants, formas o flujos de obtener el token, que autoridades y ámbitos o scopes se le asigna al token.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package io.github.picodotdev.blogbitix.springoauth.oauth;
...
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private JwtAccessTokenConverter tokenConverter;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
@Bean
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("1234567890");
return converter;
}
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter tokenConverter) {
return new JwtTokenStore(tokenConverter);
}
@Bean
DefaultTokenServices tokenServices(TokenStore tokenStore, JwtAccessTokenConverter tokenConverter) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setTokenEnhancer(tokenConverter);
return tokenServices;
}
@Bean
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore, ClientDetailsService clientDetailsService) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
@Bean
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client")
.secret("{noop}1234567890")
.resourceIds("service")
.authorizedGrantTypes("client_credentials")
.authorities("CLIENT")
.scopes("read");
}
}

El servidor OAuth de ejemplo se inicia con el comando ./gradlew oauth:run. Para obtener un token se realiza con las siguientes peticiones. Por defecto, se solicita autenticación basic pero la invocación al método allowFormAuthenticationForClients() hace que los parámetros de las credenciales se puedan indicar por parámetros.

Con el endpoint /oauth/check_token se decodifica el token. En la página de JWT hay una herramienta para decodificar el token y verificar de la firma introduciendo clave de firma en la casilla.

1
2
3
4
5
6
7
$ curl -X POST -u "client:1234567890" -d "grant_type=client_credentials" "http://localhost:8095/oauth/token"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ0MSwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEwMzE0NTk4LTRjZDctNDRmNi1hMmM4LTNjYjA5MGE1MjUxZSIsImNsaWVudF9pZCI6ImNsaWVudCJ9.n8Dwcd8YTms2Hl0YgTho9QdBWD1hAnOEmkcS-Wefy6c","token_type":"bearer","expires_in":43199,"scope":"read","jti":"10314598-4cd7-44f6-a2c8-3cb090a5251e"}
$ curl -X POST "http://localhost:8095/oauth/token?grant_type=client_credentials&client_id=client&client_secret=1234567890"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw","token_type":"bearer","expires_in":43199,"scope":"read","jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8"}
$ curl -X POST -u "client:1234567890" -d "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw" http://localhost:8095/oauth/check_token
{"aud":["service"],"scope":["read"],"active":true,"exp":1549692458,"authorities":["CLIENT"],"jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8","client_id":"client"}
Token JWT codificado y decodificado

Servidor Gateway

El servidor gateway en realidad no interviene en la lógica de OAuth porque la autorización se delega en cada servicio que contiene el recurso. Como se indicaba en Idealista estaría bien que el gateway librase de la responsabilidad de autorización a los servicios de los recursos para hacerlos más sencillos, creo que Spring Security en el momento del artículo no está soportado en Spring WebFlux que utiliza el gateway.

Lo único necesario par definir el gateway son las dependencias del proyecto, poco más que spring-cloud-starter-gateway, y la configuración de enrutado que matchea peticiones según el parámetro predicates, reescribe la URL hacia el servicio según el filtro RewritePath y finalmente redirige la petición a la ubicación del servicio indicada en uri. Se inicia con ./gradlew gateway:run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins {
id 'application'
id 'org.springframework.boot' version '2.0.8.RELEASE'
}
mainClassName = 'io.github.picodotdev.blogbitix.springoauth.gateway.Main'
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')
def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
compile('org.springframework.cloud:spring-cloud-starter-gateway', excludeSpringBootStarterLogging)
runtime('com.google.code.gson:gson:2.8.5')
runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
server.port: 8090

spring:
 cloud:
 gateway:
 routes:
 - id: path_route
 uri: http://localhost:8080/
 predicates:
 - Path=/service/
 filters:
 - RewritePath=/service/, /

Servicio, servidor de recurso

Dado que el servicio interpreta los tokens JWT y aplica reglas de seguridad necesita las mismas dependencias que utiliza el servidor OAuth.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
plugins {
id 'application'
id 'org.springframework.boot' version '2.0.8.RELEASE'
}
mainClassName = 'io.github.picodotdev.blogbitix.springoauth.service.Main'
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')
def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
compile('org.springframework.boot:spring-boot-starter-security', excludeSpringBootStarterLogging)
compile('org.springframework.cloud:spring-cloud-starter-oauth2', excludeSpringBootStarterLogging)
compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE', excludeSpringBootStarterLogging)
compile('org.springframework.security:spring-security-jwt:1.0.10.RELEASE', excludeSpringBootStarterLogging)
runtime('com.google.code.gson:gson:2.8.5')
runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
runtime('javax.xml.bind:jaxb-api:2.3.0')
runtime('com.sun.xml.bind:jaxb-impl:2.3.0')
runtime('org.glassfish.jaxb:jaxb-runtime:2.3.0')
runtime('javax.activation:activation:1.1.1')
}

El recurso es muy simple, solo devuelve un mensaje.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.springoauth.service;
...
@RestController
public class DefaultController {
private Random random;
public DefaultController() {
this.random = new Random();
}
@RequestMapping("/")
public String home(HttpServletRequest request) throws Exception {
return String.format("Hello world (%s)", request.getRequestURL());
}
}

El servicio comparte configuración similar al servidor de Ouath par el JwtAccessTokenConverter, TokenStore y DefaultTokenServices. En el método configure se define que el endpoint / requiere el rol CLIENT que se obtiene del token JWT enviado. Hay que utilizar la anotación @EnableResourceServer, se inicia con el comando ./gradlew service:run.

Hay que recalcar que el servicio para verificar el token y comprobar la autorización no necesita comunicarse con el servidor OAuth toda la información que necesita está en el token.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package io.github.picodotdev.blogbitix.springoauth.service;
...
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private JwtAccessTokenConverter tokenConverter;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
@Bean
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("1234567890");
return converter;
}
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter tokenConverter) {
return new JwtTokenStore(tokenConverter);
}
@Bean
DefaultTokenServices tokenServices(TokenStore tokenStore, JwtAccessTokenConverter tokenConverter) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setTokenEnhancer(tokenConverter);
return tokenServices;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenServices).resourceId("service");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").hasAuthority("CLIENT");
}
}

Si no se envía el token JWT se produce un error de autenticación con código de error 401 Unauthorized, si se envía un token correcto y la autoridad requerida del recurso la petición se devuelve el mensaje u el código de estado 200 OK, si se envía un token JWT con una autoridad que no corresponde con la necesaria para el recurso, en el ejemplo una autoridad DUMMY, se devuelve un código de estado 403 Forbbiden.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ curl -v http://localhost:8090/service/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8090 (#0)
> GET /service/ HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< transfer-encoding: chunked
< Cache-Control: no-store
< Pragma: no-cache
< WWW-Authenticate: Bearer realm="service", error="unauthorized", error_description="Full authentication is required to access this resource"
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Date: Fri, 08 Feb 2019 18:58:03 GMT
<
* Connection #0 to host localhost left intact
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw" http://localhost:8090/service/
Hello world (http://localhost:8080/)
$ curl -X POST -u "client:1234567890" -d "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc" http://localhost:8095/oauth/check_token
{"aud":["service"],"scope":["read"],"active":true,"exp":1549692458,"authorities":["DUMMY"],"jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8","client_id":"client"}
$ curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc" http://localhost:8090/service/
{"error":"access_denied","error_description":"Access is denied"}
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8090 (#0)
> GET /service/ HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.63.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2x
pZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc
>
< HTTP/1.1 403 Forbidden
< transfer-encoding: chunked
< Cache-Control: no-store
< Pragma: no-cache
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Date: Fri, 08 Feb 2019 19:02:14 GMT
<
* Connection #0 to host localhost left intact
{"error":"access_denied","error_description":"Access is denied"}

Los tokens JWT además de firmar se pueden cifrar, en el ejemplo se usa una conexión no segura con el protocolo HTTP usando una conexión segura HTTPS ya se proporcionaría confidencialidad para los tokens y es lo recomendado.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew oauth:run, ./gradlew gateway:run, ./gradlew service:run.

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

Retrospectiva al resaltado de sintaxis

febrero 07, 2019 11:30

Después de publicar Resaltado de sintaxis en Take Command me puse a pensar en lo impresionante que fue esta funcionalidad a nivel de programación. Recuerdo que primera vez que vi el resaltado de sintaxis tal y como lo entendemos actualmente fue con Quick BASIC 4.5. El editor de código que traía su IDE te formateaba […]

La entrada Retrospectiva al resaltado de sintaxis aparece primero en Bitácora de Javier Gutiérrez Chamorro (Guti).

» Leer más, comentarios, etc...

Blog Bitix

9º aniversario del blog

febrero 05, 2019 07:00

Hugo

Ya han pasado nueve años desde que el sábado 6 de febrero del 2010 cree un blog, El blog de pico.dev, que fue el precedente de Blog Bitix cambio motivado por pasar de Blogger a Octopress con GitHub Pages y posteriormente con Hugo.

Dicen que las bitácoras o blogs están en decadencia por las redes sociales como Twitter u otras formas de compartir contenido como los podcast que son más cálidos por la voz humana que los fríos blogs y vídeos de YouTube dinámicos más útiles en ciertos casos que los estáticos blogs, otras formas son a través de LinkedIn o Facebook. Sin embargo, y a pesar de todo creo que los blogs son una de las mejores formas de compartir contenido simplemente por el hecho de que es un contenido indexable por los buscadores cosa que ni los podcast ni los vídeos de YouTube cumplen en la misma medida a los cuales se llega por recomendación y no por búsqueda.

En Twitter últimamente se ha puesto de moda escribir mensajes encadenados e hilos, algunos son un contenido muy bueno pero personalmente me gustan poco estos hilos por el simple hecho de la misma naturaleza de Twitter de transmitir la información, no son indexables por los buscadores, tienen un momento de gloria cuando son compartidos con retwets pero al cabo del tiempo se pierden, una pena. LinkedIn pide registrarse para acceder a su contenido y Facebook ¡no, gracias! por motivos de privacidad. Por ello no he tenido hasta el momento anhelos de compartir el contenido que elaboro en cualquier otro de estos otros formatos.

También han surgido los canales de Telegram para grupos de usuarios por ejemplo para Arch Linux, GNOME, Linux, … que ofrecen ayuda sobre estos temas, vamos lo que son los antiguos foros web pero en un formato mucho peor, es muy probable que recurrentemente se pregunte cada cierto tiempo la misma pregunta y las contestaciones no quedan disponibles para otros usuarios en el futuro. Las redes sociales ofrecen inmediatez, mayor disponibilidad con los dispositivos móviles y contacto directo pero como formatos para contenido me parecen un atraso con respecto a la web y los blogs.

Desistid de compartir contenido que debiera perdurar y ser encontrado en las anteriores formas, su éxito se debe a que en ellas están los usuarios, que solo es necesario una cuenta en esos servicios y se obtiene respuesta rápidamente, pero para mí ¡larga vida a la web, HTML, los blogs y a los bloggers!.

Lo que sí podría intentar hacer es escribir artículos en inglés, alguno ya escribí a modo de prueba y es que el público potencial sería todo el mundo en vez de solo los hispanohablantes. Sería una buena forma de que practicase el inglés pero quizá lo determinante es que me requeriría mayor tiempo para escribir el artículo. El público sería mayor pero también hay mucha más competencia al haber más blogs en ese idioma.

Cada letra, cada tilde, cada coma y punto, cada imagen de un artículo está ahí y en la posición en la que está porque así alguien lo quiso e hizo con cada pulsación de tecla y clic de ratón. Uso varias herramientas para publicar el contenido pero no hay ninguna que me ayude a escribir el contenido de los artículos y crear los ejemplos, es todo manual y lleva horas hacerlo, este artículo entre tres y cuatro horas y solo contiene unos pocos párrafos, más de cinco horas los que llevan ejemplo y más aún si tengo que investigar para hacerlo. Múltiplica eso por 380 artículos escritos hasta el momento y 566 sumando los de El blog de pico.dev y da una buena cantidad de horas que no son parte mi trabajo sino simplemente en mi caso un entretenimiento.

En los artículos que escribo no suelo expresar habitualmente mis sentimientos o temas personales cosa que algunos otros bloggers hacen, puede crear un vínculo emocional entre blogger y lectores pero simplemente no es mi forma de escribir contenido. Podría también escribir menos artículos pero más elaborados o de mayor aporte, en buena medida nada de lo que escribo es nada que no se encuentre sin mucha dificultad en otro lado, eso sí si muestro algo de código suelo incluir en GitHub el código fuente del ejemplo completo cosa que ya no es tan habitual encontrar.

He llegado al noveno año y con los artículos que tengo escritos en borrador y pendientes de publicar creo que llegaré a la cifra más redonda de la década. El artículo anterior, el #379 de Blog Bitix lo tenía escrito en borrador desde el año 2016, casi dos años, es un ejemplo del tiempo que tengo algunos sin publicar, si los contabilizo para ser más exacto son 70 artículos que tengo escritos en borrador y pendientes de publicar que es lo que más me exige. Por lo que sí creo que llegaré al año 10 publicando artículos.

» Leer más, comentarios, etc...

Fixed Buffer

Herramientas de desarrollo: Travis CI

febrero 05, 2019 09:00

netCoreTravis

Hace ya unos meses, hice mis primeras publicaciones sobre tecnología en el blog de un compañero de trabajo, y en ellas hablaba de una práctica que por desgracia, sigue siendo una gran desconocida cuando hablo con gente que se dedica al software. Me estoy refiriendo a la integración continua (CI).

Si bien es cierto, que las pruebas unitarias tienen una funcionalidad clara, no valen de nada si no las ejecutas. Esto puede pasar por muchas razones, pero una de ellas (al menos la que más me suele afectar a mi), es que trabajas sobre una funcionalidad que hay que pasar a la rama principal urgentemente y no hay tiempo de lanzar todas las pruebas unitarias. Durante el desarrollo de esa característica, compruebas que todo funciona bien, y subes tus cambios tranquilo, hasta que de repente… ¡¡Algo se ha roto en otro sitio!!

Gracias a herramientas como AppVeyor , Travis CI o Azure Pipelines, (los 3 son gratis si nuestro repositorio es público) podemos conseguir automatizar este trabajo, de modo que no tengamos que preocuparnos de compilar y ejecutar las pruebas. Ademas, estos sistemas nos aportan otra ventaja adicional, podemos compilar y ejecutar pruebas en diferentes entornos, como veremos a lo largo de esta entrada sobre Travis, y la siguiente que será sobre Azure Pipelines (y como se puede ver en mi colaboración en MascandoBits sobre AppVeyor).

Antes de continuar, es un buen recordar la entrada sobre Mocking (¡o leerla sin aun no lo has hecho!), ya ahora vamos a ver las ventajas de no depender de recursos externos para ejecutar las pruebas unitarias. De hecho, vamos a utilizar ese mismo repositorio para incluir la integración continua con Travis y Azure Pipelines al ser un proyecto NetCore (podemos ejecutarlo en Windows, Linux y MacOS) y tener pruebas unitarias.

Funcionamiento de los servicios CI

En primer lugar, podemos preguntarnos como funcionan estos sistemas de CI. Si utilizas GitHub es muy simple, están totalmente integrados, y con unos pocos clicks podemos hacer que cada vez que hagamos un push al repositorio (o nos manden un Pull Request), automáticamente se lance la compilación y ejecución de las pruebas del código, sin tener que preocuparnos de nada más. Ademas, estos servicios suelen trabajar usando un fichero de configuración “.yml”, en el cual definimos todos los pasos que queremos ejecutar, de modo que es fácil reutilizar las configuraciones en nuestros diferentes proyectos con unos cambios mínimos. Una vez terminen, veremos en el historial de commits un indicador de si la integración ha ido bien o no, al igual que en los PR:

Historial Commits
Prueba PR

Ademas, estos servicios suelen permitirnos utilizar badges que podemos añadir a nuestro readme.md para saber siempre a simple vista el estado del proyecto:

Readme.md

Añadiendo Travis CI a nuestro repositorio

Para añadir nuestro repositorio a los trabajo de integración de Travis, en primer lugar, vamos a ir a su web y vamos a pulsar en el botón de registrarnos con GitHub. Nos lanzará una ventana para confirmar que autorizamos la consulta de datos desde GitHub, y una vez aceptemos, nos mostrará una ventana (¿vacía?) con nuestros proyectos. Lo que vamos a hacer, es pulsar sobre el botón “+” para añadir un nuevo repositorio:

Add Project

Sincronizamos nuestra cuenta para que se nos muestren los repositorios, y simplemente, activamos el repositorio que nos interesa:

Add Repo

Con esto, ya hemos indicado a Travis que queremos que lance el CI cada vez que detecte cambios en el repositorio, y Travis se encarga de configurarlo todo en GitHub para que le notifique esos cambios. A partir de este momento, cada vez que hagamos un push al remoto, se iniciara una compilación. En este momento, fallará al no tener el “.yml”, asi que vamos a añadirlo.

Fichero .travis.yml

Vamos a crear un fichero en el repositorio que se llamará .travis.yml (el primer “.” delante de travis hay que ponerlo también). En el vamos a poner lo siguiente:

 
# Lenguaje que vamos a usar
language: csharp
dist: trusty
mono: none
# Version del SDK de NetCore que queremos utilizar
dotnet: 2.1.301
# Matriz de sistemas operativos sobre los que queremos lanzar el CI
os:
  - linux
  - osx
# Comando que queremos ejecutar ANTES de compilar  
install:
- dotnet restore
# Script de compilacion
script:
  # Compilamos el proyecto
- dotnet build
  # Ejecutamos las pruebas unitarias
- dotnet test PruebasUnitarias/PruebasUnitarias.csproj
# No queremos que nos notifique por email los resultados
-notifications:
-  email: false

Si nos fijamos, simplemente le estamos indicando al servicio que es lo que queremos que haga. Le indicamos el lenguaje, le indicamos la versión del SDK, y los sistemas operativos sobre los que trabajar.
En el script de instalación (previo a compilar), ejecutamos un restore, para descargar los paquetes Nuget necesarios (sino, vamos a tener un fallo de compilación), y después indicamos que queremos lanzar dotnet build para compilar, y dotnet test “proyecto de pruebas” para ejecutar los test. Os dejo el enlace a la documentación para que podáis consultarla si queréis hacer cosas más concretas.

Una vez que hemos añadido el fichero, commit y para el repositorio, con lo cual, se iniciará automáticamente el trabajo. Si todo va bien, una vez que termine, si vamos a travis, veremos algo como esto:

CI End

Como comentaba antes, Travis nos permite generar badges para saber el estado del repo, esto se hace tan fácilmente como pulsando sobre el badge, y nos mostrara una ventana para seleccionar donde lo queremos poner. Simplemente, tenemos que seleccionar donde lo vamos a colocar, y nos dará el código copy-paste.

badge url

Con esto, ya tenemos una primera aproximación a la Integración Continua con Travis CI. En las siguientes entradas de esta serie sobre CI/CD, veremos Azure Pipelines, y hablaremos sobre el despliegue continuo (CD), que es tan facil como el CI, y nos permite por ejemplo, automatizar la publicación de paquetes nuget, webs, subir los binarios a un FTP, etc…

**La entrada Herramientas de desarrollo: Travis CI se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Variable not found

La interfaz IMiddleware: Middlewares tipados per request en ASP.NET Core

febrero 05, 2019 07:30

ASP.NET CoreTradicionalmente los middlewares de ASP.NET Core los hemos implementado como clases independientes más o menos con la siguiente pinta:
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context)
{
// Hacer algo antes de pasar el control al siguiente middleware
await _next(context); // Pasar el control al siguiente middleware
// Hacer algo después de ejecutar el siguiente middleware
}
}
Estas clases no heredan de ninguna otra ni implementan interfaces proporcionadas por el framework, aunque atienden a convenciones simples, como la recepción en el constructor del delegado al siguiente middleware en el pipeline, o la existencia de un método Invoke() o InvokeAsync(), que es donde introduciremos nuestra lógica, recibiendo el contexto HTTP.

La ausencia de interfaces o clases base aporta flexibilidad, pero elimina las ayudas propias del tipado fuerte y puede ser fuente de problemas si no atendemos a estas convenciones con cuidado. Es decir, si en lugar del método Invoke() por error escribimos Invke(), nada fallará en compilación. Tendremos que esperar a ejecutar la aplicación para que explote.

También es importante tener en cuenta que una clase middleware sólo es instanciada una vez, cuando la aplicación está arrancando; luego, en cada petición será ejecutado su método Invoke() sobre la misma instancia, lo que es a priori muy poco intuitivo y puede causarnos algún dolor de cabeza si no somos cuidadosos.

Por ejemplo, en el constructor no podemos recibir dependencias con un lifetime per request, como podría ser un contexto de Entity Framework, porque en el momento de creación de la instancia ni siquiera se han recibido peticiones todavía. Es decir, el siguiente código fallará al intentar inyectar un componente registrado como scoped:
public class AuditMiddleware
{
private readonly RequestDelegate _next;
private readonly IMyDataContext _dataContext;
public AuditMiddleware(RequestDelegate next, IMyDataContext dataContext) // Fallará
{
_next = next;
_dataContext = dataContext;
}

public async Task Invoke(HttpContext httpContext)
{
_dataContext.Auditing.Add(
new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
);
await _dataContext.SaveChangesAsync();
await _next(httpContext);
}
}
De hecho, las dependencias scoped deben añadirse como parámetros al método Invoke(). El siguiente código sí funcionaría correctamente, porque la referencia al contexto de datos IMyDataContext se recibe en el interior de un scope delimitado por la petición actual:
public class AuditMiddleware
{
private readonly RequestDelegate _next;
public AuditMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext httpContext, IMyDataContext dataContext)
{
dataContext.Auditing.Add(
new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
);
await dataContext.SaveChangesAsync();
await _next(httpContext);
}
}

La interfaz IMiddleware

ASP.NET Core proporciona una fórmula para mejorar un poco la forma de crear middlewares: la interfaz IMiddleware. Esta se encuentra definida en el espacio de nombres Microsoft.AspNetCore.Http de la siguiente forma:
public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}
Las clases middleware que implementen IMiddleware presentan dos diferencias principales respecto a las "clásicas":
  • Primero, el uso del tipado fuerte nos evitará fallos en tiempo de ejecución, puesto que estaremos obligados a implementar el método InvokeAsync(), que es desde donde se procesarán las peticiones. Fijaos además que se recibe tanto el contexto HTTP como el delegado que nos permitirá ceder el control al siguiente middleware del pipeline.
     
  • Segundo, las instancias de este middleware serán solicitadas al contenedor de servicios de ASP.NET Core en cada petición a través de una factoría. Es decir, no se trata de una instancia única como teníamos en el caso anterior. Como consecuencia:

    • Debemos registrar obligatoriamente el middleware en el método ConfigureServices() de la clase Startup para que esté disponible. Si no es así, la aplicación explotará cuando llegue la primera petición porque el framework no será capaz de crear la instancia.
       
    • Su ciclo de vida será el que especifiquemos en el momento de registrarlas. Lo habitual será hacerlo como transient, pero podríamos elegir otra estrategia si es necesario.
       
    • Dado que es el propio framework el que creará las instancias, podemos utilizar inyección de dependencias en el constructor.
Por tanto, un middleware como el anterior podríamos reescribirlo usando IMiddleware de la siguiente forma:
public class AuditMiddleware: IMiddleware
{
private readonly IMyDataContext _dataContext;
public AuditMiddleware(IMyDataContext context)
{
_dataContext = context;
}

public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
{
_dataContext.Auditing.Add(
new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
);
await _dataContext.SaveChangesAsync();
await next(httpContext);
}
}
Y para registrarlo y añadirlo al pipeline, lo haríamos de forma similar a lo habitual, por ejemplo:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<AuditMiddleware>();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseMiddleware<AuditMiddleware>();
...
}
Como nota negativa, decir que los middlewares que implementan IMiddleware no permiten el envío de parámetros de inicialización personalizados. Es decir, un código como el siguiente fallará en tiempo de ejecución:
app.UseMiddleware<MyMiddleware>(new MyMiddlewareOptions { ... });
En definitiva, se trata de una fórmula adicional, denominada oficialmente factory-based middlewares, para implementar middlewares que soluciona algunos problemas propios del enfoque basado en convenciones. Aunque de momento limitado por la imposibilidad de enviar parámetros de inicialización, sin duda es una buena opción a tener en cuenta en casos en los que esto no es necesario.

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 348

febrero 04, 2019 07:30

Enlaces interesantes
Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Otros

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Koalite

Sistemas de control de versiones: algo más que comandos en un shell

febrero 04, 2019 05:06

Como contaba en el post sobre las tecnologías que usé en 2018, uno de los cambios fundamentales a nivel profesional ha sido la introducción de git como sistema de control de versiones. Técnicamente soy usuario de git desde 2011 (o eso dice mi perfil de GitHub), pero lo cierto es que el uso que le había dado hasta ahora no era demasiado completo. Sí, me servía para gestionar código, crear algunas ramas, incluso hacer algún Pull Request, pero sin tratar de entender realmente lo que había por debajo.

Viniendo de usar Subversion durante muchos años (y de estar muy cómodos con él), antes de comenzar a utilizar git dedicamos bastante tiempo a conocerlo mejor y tratar de averiguar qué nos podría ofrecer, más allá de replicar las cosas que ya podíamos hacer con Subversion (pero más rápido), y a decidir qué necesitábamos de él.

Eso me ha hecho replantearme algunas ideas que tenía sobre lo que esperaba de un sistema de control de versiones y el uso que le podía dar.

Por supuesto, todo el mundo sabe un sistema de control de versiones sirve para, ejem, controlar versiones. Es decir, poder almacenar distintas versiones de un conjunto de ficheros y obtener información sobre ellas: qué cambios se introdujeron, cuándo, quién los introdujo, etc. Que esto es una de las cosas básicas que cualquier desarrollador debería conocer está fuera de toda duda.

Pero además de eso, un sistema de control de versiones habilita otro tipo de escenarios, y dependiendo del sistema de control de versiones que usemos y de cómo lo usemos, tendremos más o menos facilidades para trabajar de distintas formas.

A la hora de decidir cómo queremos trabajar con un sistema de control de versiones tenemos que pensar en lo que queremos conseguir. En qué tipo de información queremos tener, cómo vamos a consumirla y quién la va a consumir. En qué flujos de trabajo necesitamos soportar para permitir la colaboración entre distintos miembros de un mismo equipo. En cuál es el ciclo de vida de nuestra aplicación y cómo vamos a gestionarlo desde el punto de vista del código.

El flujo del tiempo

Lo primero que esperas de un control de versiones es poder consultar el historial de cambios que se han ido produciendo. La historia del proyecto. Cuando uno piensa en la historia, es fácil verla como un registro lineal de cosas que han pasado en el Mundo Real&trade. Parece lógico.

Viéndolo así, a través de la historia del repositorio podríamos saber todos los pasos que hemos ido dando hasta alcanzar el aspecto actual del código, incluyendo los posibles pasos intermedios erróneos que dimos mientras buscábamos la solución a un problema o el mejor diseño para ese componente que nos costó un par de pruebas hasta que lo cuadramos.

Para una funcionalidad cualquiera podríamos tener una historia como ésta:

v7- Actualizado esquema de base de datos
v6- Ajustado UI a cambios del modelo
v5- Refactorizado modelo de dominio
v4- Modificado UI (falta botón de guardar)
v3- Incluidos más tests sobre dominio
v2- Añadida parte de persistencia
v1- Implementación inicial de modelo de dominio

Esto tiene sus ventajas porque mantienes el máximo nivel de detalle posible y puedes volver a cualquier instante de tiempo del desarrollo (siempre y cuando lo registraras en el sistema de control de código fuente, claro). A cambio, puede resultar difícil saber exactamente en qué estado se encontraba la aplicación en cada momento porque puedes encontrarte con versiones intermedias en las que algo estaba todavía a medio implementar, o con una implementación que no acabó siendo la definitiva.

Podemos plantearnos si nos interesa reescribir la historia. ¿Realmente necesitamos todos esos pasos intermedios en nuestra historia? ¿Podemos eliminar el ruido, perder granularidad, y quedarnos sólo con versiones “con sentido” de la aplicación, por ejempo aquellas en las que se implementó una funcionalidad completa o se corrigió un bug?

Depende. Depende mucho de para qué queremos la historia y quién la vaya a consumir.

Si la historia va a ser consumida únicamente por los desarolladores del proyecto, mantener la máxima granularidad parece recomendable. Sí, puede que queden rastros de pasos en falso o de etapas intermedias del desarrollo, pero tener toda esa información nos puede servir para comprender mejor por qué el código ha acabado siendo lo que ha acabado siendo.

En cambio, si queremos que la historia sirva como documentación para agentes externos, quizá ésta no sea la mejor aproximación. Si nuestra intención es que la historia la pueda comprender un equipo de QA que va realizando pruebas sobre la aplicación, introducir todo ese ruido no les va a ayudar y no van a tener muy claro qué tienen que probar y cuándo pueden empezar a probar una funcionalidad. Algo similar ocurre si pretendemos que la historia permita a otros equipos de desarrollo que dependan del nuestro ir adaptando su código a nuestros cambios.

Para esos escenarios, parece más adecuado tener una historia más parecida a esta:

v3- Bug #444: Se permitía guardar cliente sin dirección
v2- Posibilidad de asociar precios especiales a proveedores
v1- Informe de Pagos por Cliente

Cada versión contiene un conjunto de cambios completo para implementar una funcionalidad y/o corregir un fallo. Por el camino hemos perdido detalle de la forma en que se fue implementando cada cosa, pero la historia nos queda “más bonita”.

Si quieres conseguir una historia de este estilo sin tener que renunciar a poder tener versiones intermedias mientras estás trabajando, muchos sistemas de control de versiones te permiten ir generando las versiones que quieras y, antes de ponerlas en común con el resto del equipo, convertir todas esas revisiones en una única revisión que incluya todos los cambios.

Dependiendo del sistema de control de versiones que utilices y la forma en que trabajes con él hay alternativas de tener algo a medio camino.

En cualquier caso, es interesante empezar a considerar la historia como un artefacto más generado durante el proceso de desarrollo y analizar de qué forma podemos sacarle el máximo partido dependiendo de nuestras necesidades.

Universos paralelos

Sin entrar en disquisiciones (meta)físicas, la mayoría de las veces consideramos el tiempo como lineal. Fluye en una sola dirección y los acontecimientos se suceden unos detrás de otros. Eso cuadra bastante con la idea de historia en un control de versiones, pero al hablar del estado de la aplicación es habitual que, en un instante de tiempo dado, no tengamos un único estado.

Un caso claro es si tenemos dos desarrolladores trabajando en distintas áreas de la aplicación. Si los cambios que están introduciendo se van registrando en un sistema central, coexistirán, al menos, dos versiones distintas de la aplicación: una por cada desarrollador.

También es posible que necesitemos mantener en paralelo varias versiones de la aplicación, por ejemplo porque a la versión N que está en producción podemos aplicarle correcciones de errores mientras desarrollamos la versión N+1 con nuevas funcionalidades. O porque tenemos el típico produyecto con versiones ligeramente distintas de código para cada cliente.

Generalmente esto se resuelve mediante el uso de ramas en el sistema de control de versiones, lo que nos permite mantener esos mundos paralelos evolucionando por separado, posiblemente cada uno a su ritmo, y decidir en qué momentos se junta o separan.

Igual que ocurre con la historia, a la hora de establecer la estrategia de ramas que vamos a utilizar necesitamos pensar qué esperamos obtener de ella.

Hace falta considerar la estabilidad que queremos que tenga cada rama. Podemos tener ramas extremadamente estables, donde se supone que el código está siempre listo para desplegar en producción. Otras con estabilidad algo menor, con el código listo para desplegar en un entorno de QA. Otras en las que permitimos código inacabado que podría no pasar los tests o incluso no compilar, pero que usamos para facilitar la colaboración entre varios desarrolladores.

Entre todas estas ramas, necesitaremos establecer políticas que nos aseguren que se mantiene la estabilidad deseada, marcando la forma en que se traspasan cambios entre ellas y la forma en que se pasan esos cambios.

Se pueden crear flujos de trabajo muy elaborados y burocratizados para gestionar todo este mundo de universos paralelos. Es fácil encontrar ejemplos en internet, por ejemplo el archiconocido Git Flow, pero antes de aplicar ciegamente uno de ellos, merece la pena dedicar un tiempo a evaluar las necesidades reales de tu proyecto.

Muchas veces estos flujos de trabajo son demasiado generalistas y cubren escenario que no necesitas, introduciendo una complejidad y fricción adicional durante el desarrollo.

Quizá tu aplicación no necesite tener ramas dedicadas a estabilizar la aplicación antes de cada nueva versión porque estás usando un sistema de despliegue continuo. O te puedas ahorrar crear ramas para cada funcionalidad/bug porque todo el mundo desarrolla directamente sobre la rama principal para asegurar que la integración del código se realiza realmente cada poco tiempo.

Conclusión

Cuando empezamos a pensar en sistemas de control de versiones es fácil centrarnos en características puramente técnicas: qué operaciones soporta, cómo funciona cada una de ellas, qué clientes existen y cómo se manejan, qué rendimiento ofrece… Está muy bien saber todo lo que se puede hacer con un sistema de control de versiones, pero es aún más importante saber lo que necesitas hacer con él.

A la hora de decidir cómo lo va a usar debes analizar para qué quieres la historia que se genera y quién la va a consumir, porque ello hará que sea más o menos práctico utilizar determinadas características de tu sistema de control de versiones. Pensar en la historia como un artefacto más del desarrollo (igual que el código fuente) y no sólo como un subproducto del mismo puede abrirte la puerta a escenarios interesantes.

Si decides que vas a utilizar ramas para mantener flujos de trabajo en paralelo (algo que tampoco es obligatorio), piensa los escenarios que necesitas habilitar con ellas de cara a facilitar la colaboración entre miembros del equipo y el mantenimiento de versiones de producto. Te en cuenta la estabilidad que requiere cada rama, el tiempo que vivirá y la forma en que traspasarás cambios de unas ramas a otras. Evita guiarte ciegamente por la metodología de turno que, sí, es muy completa y está muy bien pensada, pero también puede resultar excesivamente compleja para tu caso de uso.

Posts relacionados:

  1. PhoneGap/Cordova por línea de comandos
  2. Sistemas de Tipos: Más allá de Java y C#

» Leer más, comentarios, etc...

Fixed Buffer

SourceLink: Depuración de código bajo demanda

febrero 01, 2019 09:30

Después de la última entrada en colaboración con VariableNotFound, volvemos a la normalidad, y hoy vengo a hablaros de una herramienta relativamente nueva, que me parece muy interesante conocer para nuestros proyectos, esta herramienta es SourceLink.

¿Y que es esta herramienta?, te puedes estar preguntando, pues fácil, es una herramienta de descarga de código bajo demanda, la cual nos permite que si quien desarrolla el paquete hace los deberes (hay que dar tiempo de adaptación también eh, ¡no nos pongamos nerviosos!), nosotros podamos entrar a depurar el código fuente, porque este se nos descargue automáticamente desde el repositorio. Ojo, esto lo que nos permite es entrar y seguir la ejecución, no modificar el paquete en sí mismo, pero esto al menos, nos permite saber si el fallo es nuestro o del autor. (Y creedme, ¡esto es mucho mejor que tener que descargarse el proyecto entero y depurarlo de verdad!)

Dicho esto, vamos a meternos en faena… ¿Como puedo habilitar SourceLink en mi Visual Studio?

Habilitar SourceLink en Visual Studio

Esto en realidad es muy fácil, simplemente tenemos que ir al menú “Herramientas→Opciones”, y dentro de la ventana que nos abre, bajar hasta “Depuración→General” y buscar “Habilitar compatibilidad con vínculos de origen”, una vez lo encontremos, tenemos que activarlo:

SourceLink

Con esto tan simple, ya lo hemos activado, pero ahora, vamos a ver cómo funciona. Por ejemplo, yo he utilizado un paquete Nuget que hice en su día para WoL, y que hace unas semanas actualicé para dar soporte a SourceLink. Para ello, creamos un proyecto, e instalamos el paquete con el comando:

PM-> Install-Package EasyWakeOnLan

Y por ejemplo, podemos utilizar este código:


string Mac = null;
//Instance the class
EasyWakeOnLanClient WOLClient = new EasyWakeOnLanClient();
//Wake the remote PC
WOLClient.Wake(Mac);

Este código, a priori debería lanzar una excepción, ya que no le indicamos una Mac válida. A modo de comparación, vamos a ejecutarlo primero sin SourceLink:

Como podíamos prever, la librería lanza una excepción, y para nosotros, toda la información se limita a la llamada de la librería, pero si volvemos a ejecutar con SourceLink habilitado, vemos que, al llegar a la línea, nos muestra el mensaje:

SourceLinkDownload

Y cuando pulsamos en “Descarga el origen y continuar depurando”, vemos que el error se lanzaba en la primera línea, al intentar operar con la variable “Mac” siendo su valor null:

InternalError

Hay que decir además, que no es necesario que se produzca una excepción para poder entrar a depurar el paquete, si entramos dentro del método como lo haríamos con uno nuestro, también nos permite descargar el código y depurar el paquete.

Como se puede ver, si eres consumidor de paquetería Nuget, esta herramienta es muy interesante, y yo recomiendo tenerla activada, ya que muchos paquetes hoy en día ya lo soportan, y puede ser de ayuda para encontrar el problema. Si eres el desarrollador de un paquete Nuget, esta opción también es interesante, ya que te pueden dar información más detallada sobre la issue que hay en tu código.

Más adelante hablaremos sobre cómo crear y publicar un paquete Nuget en Nuget.org o en un repositorio privado, y veremos más en profundidad que hacer para dar soporte a esta maravillosa herramienta que es SourceLink.

**La entrada SourceLink: Depuración de código bajo demanda se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 346

enero 29, 2019 05:22

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Otros

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Variable not found

El hosting in-process de ASP.NET Core 2.2

enero 29, 2019 10:40

ASP.NET CoreSin duda, entre las mejoras incluidas en ASP.NET Core 2.2 destaca especialmente el nuevo modelo de hosting in-process para IIS, que promete cuadriplicar el rendimiento prácticamente sin hacer ningún cambio en nuestras aplicaciones, siempre que las ejecutemos en un entorno Windows/IIS.

Como recordaréis, se trata de una mejora en el módulo ASP.NET Core (ANCM) que hace que las aplicaciones se ejecuten directamente dentro del worker del servidor web (w3wp.exe), evitando las llamadas internas producidas cuando IIS actuaba como un mero proxy inverso.

Por verlo gráficamente, el siguiente diagrama muestra la arquitectura tradicional de un sistema ASP.NET Core funcionando sobre IIS. En el modelo out-of-process utilizado hasta ASP.NET Core 2.1, cada petición realizada desde el exterior era capturada por IIS, que a su vez lanzaba una petición HTTP local con los mismos contenidos hacia Kestrel, que era quien la procesaba ejecutando nuestro código. La respuesta generada desde Kestrel era enviada de vuelta a IIS como respuesta a la petición, quien la retornaba al agente de usuario:

ASP.NET Core out-of-process

En este modelo de funcionamiento, por tanto, cada petición HTTP entrante generaba otra petición HTTP interna que, aunque estuviera dentro de la misma máquina, imponía una penalización importante en el rendimiento de las aplicaciones.

El hosting in-process, aparecido con ASP.NET Core 2.2, cambia las reglas del juego eliminando esas peticiones HTTP internas y los retardos que implicaban, lo que ha posibilitado el espectacular incremento en el rendimiento que su uso ofrece. Ahora, el módulo para IIS ejecuta internamente la aplicación y se comunica con ella de forma directa:

Hosting in-process

Este es el modo por defecto de los nuevos proyectos ASP.NET Core creados usando las plantillas estándar, algo que podemos claramente ver si echamos un vistazo al archivo .csproj de un proyecto recién creado, ya sea desde Visual Studio o desde .NET Core CLI:
  <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
Si tenéis un proyecto ASP.NET Core 2.1 o anterior y lo actualizáis a 2.2, tendréis que añadir el elemento <AspNetCoreHostingModel> de forma manual para usar este modelo de ejecución.
Sin embargo, estas mejoras espectaculares no son gratis del todo, y creo que es interesante conocer un poco mejor lo que implica asumir este nuevo modo de funcionamiento de nuestras aplicaciones y responder a algunas preguntas que podríamos hacernos por el camino.

¿Qué implica usar el modo in-process?

Pues como casi todo en la vida, asumir este nuevo modo de funcionamiento tiene sus implicaciones y, a veces, contraindicaciones ;)

Por ejemplo, una consecuencia directa es que cuando usamos este modo desde Visual Studio, ya no podremos ver desde el propio entorno la salida de consola de la ejecución del proyecto; u otro ejemplo, ya no funcionará la compilación automática al cambiar archivos de código fuente. Si para nosotros estas son características importantes, lo mejor es continuar utilizando el hosting out-of-process, al menos durante el desarrollo.

También es importante un efecto colateral algo inesperado, y que puede dar lugar a problemas. Dado que ahora el proceso se ejecuta dentro del worker de IIS, el directorio por defecto retornado por Directory.GetCurrentDirectory() no será el de nuestra aplicación, sino el del propio IIS, como C:\Windows\System32\inetsrv o C:\Program Files\IIS Express. Si nuestro código depende en algún punto de que el directorio de ejecución sea la carpeta de los binarios, fallará.

Afortunadamente tiene una solución sencilla, pues si queremos dejarlo todo en su sitio sólo debemos actualizar el directorio actual con una llamada manual a Directory.SetCurrentDirectory() cuando la aplicación arranque. Probablemente en muchos escenarios nos valga con establecerlo al ContentRootPath ofrecido por IHostingEnvironment:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
Directory.SetCurrentDirectory(env.ContentRootPath);
...
}
Asimismo, como se indica en la documentación oficial del módulo ASP.NET Core, hay otra serie de cambios a tener en cuenta, entre otras:
  • El servidor que se utilizará internamente ya no es Kestrel, sino IISHttpServer. Salvo que estemos usando características específicas de Kestrel, esto no debería afectarnos mucho.
  • La arquitectura (x86 o x64) de la aplicación y el runtime deberá coincidir con el del pool de aplicaciones.
  • El atributo requestTimeout de la configuración del módulo ya no será válido, aunque tiene sentido, pues este atributo definía el timeout de la petición interna entre IIS y Kestrel.
  • No será posible compartir app pools entre distintas aplicaciones.
  • La parada de aplicaciones al hacer web deploy o usando app_offline.html puede retrasarse si hay conexiones abiertas.
En definitiva, el mensaje es que incluso mejoras espectaculares y apetecibles como el hosting in-process pueden venir cargadas de efectos colaterales que debemos tener en cuenta. Antes de adoptarlas hay ser prudentes y probarlas cuidadosamente en nuestros escenarios, para sólo dar el paso adelante cuando estemos seguros de que todo funciona con garantías.

Y para acabar con buen sabor de boca, añadiré un punto positivo: al usar el hosting in-process podremos detectar directamente las desconexiones de los clientes. Recordaréis que esto antes no era posible porque la desconexión física se producía en un servidor (IIS) distinto al que procesaba las peticiones (Kestrel), pero, al encontrarse ahora todo dentro del mismo proceso, podremos aprovechar esta característica para implementar lógica de cancelación de peticiones. De esta forma, podremos informarnos mediante un token de cancelación si el cliente desconectó, por ejemplo para evitar la continuación de un proceso cuya respuesta no va a ser recibida por nadie:
public Task<IActionResult> GenerateComplexReport(int id, CancellationToken cancellationToken)
{
// cancellationToken will be cancelled when the client disconnects
var report = await _reportGenerator.GenerateComplexReportAsync(id, cancellationToken);
if (report == null)
{
return Empty();
}
return Ok(report);
}

¿Cómo puedo volver al modelo anterior, el hosting out-of-process?

Si por cualquier motivo quisiéramos desactivar el hosting in-process, bastaría como eliminar el elemento <AspNetCoreHostingModel> del archivo .csproj, o bien establecer su valor a OutOfProcess:
  <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
De esta forma, todo seguiría funcionando tal y como lo hacía antes de la versión 2.2 de ASP.NET Core.

¿Podemos usar el modo in-process sólo en producción?

Esto podría ser interesante si preferimos utilizar el modelo out-of-process mientras desarrollamos, pero disfrutar del incremento brutal de rendimiento una vez pasemos a producción.

Aunque en un primer vistazo el modo de alojamiento parece depender directamente del valor que establezcamos en la propiedad <AspNetCoreHostingModel> del archivo del proyecto .csproj, en realidad este valor sólo se utiliza para generar apropiadamente el web.config que será utilizado por IIS para configurar el módulo ASP.NET Core al lanzar la aplicación. Por ejemplo, este es el web.config incluido al publicar un proyecto con el modo in-process configurado en su .csproj:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\InProcessTest.dll" stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" hostingModel="InProcess" />
</system.webServer>
</location>
</configuration>
Por tanto, en tiempo de ejecución no tenemos forma de establecer uno u otro modelo de hosting porque cuando la aplicación arranque ya estará decidido, pero sí podemos hacerlo en tiempo de compilación o despliegue, que es cuando se genera el archivo web.config.

Para ello, podemos eliminar del archivo de proyecto .csproj el elemento <AspNetCoreHostingModel> y añadir el siguiente bloque, consiguiendo que el modelo de hosting sea in-process sólo cuando hayamos compilado con la configuración "Release" del proyecto, algo que normalmente ocurrirá cuando vayamos a desplegar a producción:
  <PropertyGroup Condition="'$(Configuration)' == 'Release' ">
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
De esta forma, mientras estamos en modo "Debug" estaremos usando el hosting out-of-process, mientras que el modelo in-process sólo lo usaremos al ejecutar tras compilar o desplegar en modo "Release".

¿Cómo podemos saber en tiempo de ejecución si la aplicación se está ejecutando el modo in-process?

Pues si lo necesitáis para algo, la forma más sencilla, pero probablemente válida en muchos casos, sería consultar el nombre del proceso actualmente en ejecución con una llamada a Process.GetCurrentProcess().ProcessName. Cuando estamos utilizando el hosting in-process, el nombre del proceso será "w3wp" o "iisexpress", mientras que en out-of-process recibiremos "dotnet".

Pero también podemos hacerlo de forma algo más "pro" ;) Simplemente deberíamos determinar si el módulo ha sido cargado por el proceso actual utilizando la llamada GetModuleHandle() del kernel del sistema operativo (obviamente, sólo funcionará en Windows):
public static class AspNetCoreModuleHelpers
{
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);

public static bool IsInProcess()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& GetModuleHandle("aspnetcorev2_inprocess.dll") != IntPtr.Zero;
}
}
Así, el siguiente código retornará el modo de alojamiento cuando se haga una petición a "/mode":
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseRouter(
route => route.MapGet("mode",
async ctx => await ctx.Response.WriteAsync(
"In-Process: " + AspNetCoreModuleHelpers.IsInProcess()
)
)
);
}
}
Espero que os haya resultado interesante, y que os ayude a sacar mayor provecho de esta interesante mejora de ASP.NET Core 2.2, o al menos hacerlo con mayor conocimiento de causa :)

Publicado en Variable not found.

» Leer más, comentarios, etc...

Fixed Buffer

Crear y utilizar librerías multiplataforma con C++ y NetCore (Parte 1)

enero 22, 2019 09:00

C++ y NetCore

Son ya varias entradas hablando sobre NetCore, y la potencia de un entorno multiplataforma, pero… ¿Que pasa si por necesidades de rendimiento, necesitamos aun más potencia y es requisito ejecutar código nativo en C++ y NetCore?

Pues precisamente de eso vengo a hablaros hoy, y ademas es una entrada muy especial para mi, ya que vamos a presentarla como una colaboración con el compañero José M. Aguilar de Variable Not Found, el cual, he tenido la suerte de tener como profesor de unos cursos ASP.NET MVC 5 y ASP.NET MVC Core, los cuales recomiendo sin duda (de leer su blog no digo nada por razones obvias…). Y precisamente por esa colaboración, esta entrada se va a presentar en 2 partes:

  • La primera aquí, donde vamos a hablar de como compilar código C++ multiplataforma.
  • La segunda parte en Variable Not Found, donde explicaremos las opciones para consumir las librerías sin perder la capacidad de ejecutar NetCore multiplataforma.

Hechas las presentaciones, vamos a meternos en faena.¿ Que necesitamos para poder ejecutar C++ y NetCore? Pues en primer lugar, necesitaremos una librería C++ que sea multiplataforma. Vamos a crear una librería sencilla, la cual nos permita obtener un string y hacer una suma. Después, vamos a compilarla en Windows, en Linux y en MacOS (versiones: Windows 10, Debian 9.5, MacOS High Sierra 10.13.6)

Creando el proyecto C++

Esta vez vamos a cambiar un poco la manera de trabajar, esto es porque para conseguir independencia del IDE, en C++ se utilizan los CMake, por lo que nuestro proyecto va a constar de 3 archivos:

  1. Nativo.h
  2. Nativo.cpp
  3. CMakeLists.txt

Nativo.h

En este fichero vamos a definir los prototipos de las funciones que expondrá la librería, además de hacer los ajustes necesarios para conseguir una librería multiplataforma. Para ello, crearemos una carpeta para el proyecto, y dentro de esta, un fichero que se llame Nativo y tenga de extensión “.h”. Dentro del fichero, pondremos el siguiente código: 

 
#pragma once

//Utilizamos directivas de preprocesado para definir la macro de la API
//Esto hay que hacerlo porque en Windows y en NoWindows se declaran diferente
#ifdef _WIN32
#  ifdef MODULE_API_EXPORTS
#    define MODULE_API extern "C" __declspec(dllexport) 
#  else
#    define MODULE_API extern "C" __declspec(dllimport)
#  endif
#else
#  define MODULE_API extern "C"
#endif
//Declaracion de los métodos nativos
MODULE_API void GetStringMessage(char *str, int strsize);

MODULE_API int Suma(int a, int b);

En él, vemos que la gran mayoría, son definiciones y solo dos líneas son las declaraciones de las funciones que expone la librería pero… ¿Y por qué todas esas definiciones?
Básicamente, por lo que a mi modo de ver, es la herencia de la época (por suerte superada ya) de “Absorbe, Expande y Destruye”, por lo cual, en Windows, el código nativo en C++, declara “__declspec” en sus APIs, haciendo incompatible el código nativo con el resto de plataformas UNIX, además de cambiar la extensión de la librería (pero eso lo hacen todos y es irrelevante, porque NetCore ya tiene eso en cuenta). Para más información, podéis echarle un ojo a este enlace.
Pero veámoslo, lo que estamos haciendo, es definir una macro, la cual en función de si se compila en Windows o no, añade “__declspec(dllexport)/__declspec(dllimport)” o lo deja en blanco, de modo que cuando compilemos en la plataforma concreta la librería, corra sin problemas.

Nativo.cpp

En ese fichero, vamos a colocar el cuerpo de los métodos, y es el que más familiar nos va a resultar:

 
#include "Nativo.h"
#include <iostream>
#include <algorithm>

void GetStringMessage(char* str, int strsize) {
	//Comprobamos que el tamaño del buffer que nos indican en mayor que 0
	if (strsize > 0) {
		//Definimos el mensaje
		const char result[] = "Mensaje generado desde C++";
		//Obtenemos cual va a ser la longitud maxima que podemos utilizar
		const auto size = std::min(sizeof(result), strsize) - 1;
		//Compiamos al buffer la cadena
		std::copy(result, result + size, str);
		//Indicamos el final de cadena
		str[size] = '\0';
	}
}

int Suma(int a, int b) {
	return a + b;
}

En él, vemos que se incluyen el fichero de prototipos (.h) y 2 librerías estándar, después de esto, se definen los cuerpos de los dos métodos que expone nuestra API.

CMakeLists.txt

En este fichero, indicaremos a CMake las instrucciones que debe ejecutar para generar el proyecto:

 
# Version mínima de CMake
cmake_minimum_required(VERSION 3.0)
#Nombre del proyecto
project(EjemploNativo)
#Añadimos los ficheros y le decimos que sera una librería compartida
add_library(EjemploNativo SHARED Nativo.cpp Nativo.h)
#Quitamos los prefijos (esto quita el "lib" que añade)
set_target_properties(EjemploNativo PROPERTIES PREFIX "")
#Indicamos el nombre de la salida
set_target_properties(EjemploNativo PROPERTIES OUTPUT_NAME EjemploNativo)

En él, vemos que le vamos a indicar el nombre del proyecto, los ficheros que contiene, y el nombre del binario de salida.

Compilando el proyecto

Para generar nuestro binario, utilizaremos CMake y el compilador que tengamos instalado en nuestro equipo (Visual Studio en Windows, GCC en Linux o XCode en MacOS habitualmente), para ello, lo primero será descargar CMake desde su web o mediante apt-get (en Linux).
Una vez que lo tengamos instalado (en el proceso de instalación, seleccionaremos la opción de añadir al PATH, para poder utilizar CMake por consola), vamos a la ruta donde esta el fichero CMakeLists.txt, y lanzamos una consola (o terminal, depende del OS), y ejecutamos los siguientes comandos:

 
#Para Windows (utilizo Visual Studio 2017, sería necesario indicar el vuestro)
cmake -G"Visual Studio 15 2017 Win64" 

#Para Linux o MacOS
cmake .

#Para compilar, independientemente de la plataforma
cmake --build . --target

Si nos fijamos, en Windows le tenemos que indicar el generador aunque solo tengamos un Visual Studio instalado, cosa que en Linux y MacOS no es necesario. Yo he utilizado Visual Studio 2017, pero dejo el enlace a la lista de generadores disponibles.

Si todo ha ido bien, deberíamos ver una salida como esta en nuestras terminales (Pongo imágenes de Windows 10 con PowerShell, Debian 9.5 con terminal y MacOS HighSierra con terminal):

cmake Windows
cmake linux
cmake MacOS

Como se puede ver, dentro de nuestros directorios, ya tenemos nuestra librería “EjemploNativo.XXX”

De este modo, ya hemos conseguido hacer compilación multiplataforma con nuestro código nativo. El siguiente paso, es consumir esas librerías multiplataforma desde nuestra aplicación NetCore, pero para eso, tenéis que visitar el blog del compañero José M. Aguilar donde publico la segunda parte de la entrada. En caso de que encontreis problemas durante la prueba de NetCore en Linux, hace poco hablamos sobre como depurar sobre SSH.

Como siempre, dejo el enlace al código fuente en Github, por si queréis saltaros la parte de escribir el código.

**La entrada Crear y utilizar librerías multiplataforma con C++ y NetCore (Parte 1) se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Poesía Binaria

Notifica, logea y enriquece tu experiencia de trabajo en Bash con este script

enero 21, 2019 09:27

En nuestro trabajo diario peleando con sesiones de terminal hay ocasiones en las que, teniendo una sesión de terminal abierta, no sabemos a qué hora se ejecutó un comando determinado. O acabamos de iniciar una tarea que tiene pinta de ser muy larga y nos gustaría que el ordenador nos avisara cuando termine para no estar mirando cada poco tiempo. Además, seguro que a ti también te ha pasado, te acuerdas de que necesitas el aviso cuando la tarea está iniciada y no puedes pararla.

Pequeña introducción

No pretendo crear un sistema muy complejo para este propósito, para eso tenemos auditd, del que hablaré en próximos posts. Este es un pequeño sistema que consume pocos recursos y se dedica a:

  • Escribir en el log de sistema los comandos que se van ejecutando cuando concluyen.
  • Informar en la ventana de nuestra terminal de la hora que es, de lo que ha tardado en ejecutar un cierto comando y la carga del sistema en ese momento. Podremos configurarlo y mostrar más cosas.
  • Notificar con un programa externo cuando una orden ha finalizado. Ya sea por medio de notificación de escritorio, ventana emergente, destacando la ventana de terminal, o incluso enviando un webhook, ya depende de nosotros.

Podemos ver, tras la finalización de un comando que ha tardado más de 2 segundos (por ejemplo, comando_largo) el siguiente mensaje, notificación:

Además, como ha tardado más de 10 segundos (los tiempo podremos configurarlos), veremos lo siguiente en el escritorio:

Por supuesto, podemos elegir desactivar/activar las notificaciones, o cómo se va a notificar desde otra ventana mientras la tarea está en ejecución.

El script

Pongo aquí una primera versión del script. Ya que se me ocurren muchas mejoras, y pequeños cambios que podemos hacer para enriquecer la experiencia aún más.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/bin/bash

readonly BASHISTANT_DIR=$HOME/.bashistant
readonly BASHISTANT_LOCK=$BASHISTANT_DIR/lock
readonly BASHISTANT_LOCKFD=99

BASHISTANT_COLOR_ENABLE=1
BASHISTANT_INFO_ENABLE=1
BASHISTANT_NOTIFY_ENABLE=1
BASHISTANT_LOG_ENABLE=1
BASHISTANT_TIMER_COLOR='34'
BASHISTANT_NOTIFY_TIME=10               # If command lasts more than 10 seconds, notify
BASHISTANT_NOTIFY_COMMAND="@default"
BASHISTANT_SHOW_TIME=2
BASHISTANT_NOW_FORMAT="%d/%m/%Y %H:%M:%S"
BASHISTANT_NOW_COLOR='35'
BASHISTANT_INFO_ALIGN="right"
BASHISTANT_INFO_PADDING="  "
BASHISTANT_ELAPSED_COLOR='36'
BASHISTANT_LOAD_COLOR='38'

BASHISTANT_INFO_FORMAT="[ %NOW | %ELAPSED | %CPULOAD ]"

_BASHISTANT_START=

readonly MYPID=$$

MYPIDS=()

function onexit() {
        flock -u BASHISTANT_LOCKFD
        [ ! -r "$BASHISTANT_LOCK" ] || rm -f "$BASHISTANT_LOCK"
        echo "Ocurrió un problema y el programa se cerró inesperadamente" >2
        logger "Bashistant: There was a problem here"
}

function __bashistant_init() {
        [ -d "$BASHISTANT_DIR" ] || mkdir "$BASHISTANT_DIR"
        eval "exec $BASHISTANT_LOCKFD>"$BASHISTANT_LOCKFD"";
        readonly WINDOWPID=$(ps -o ppid,pid| grep $$ | awk '{print $1;exit}')
        if xset q &>/dev/null && hash xdotool; then
                readonly WINDOWID=$(xdotool search --pid $WINDOWPID | tail -1)
        fi
        COMMANDS=()
}

__bashistant_init

function __bashistant_get_timestamp() {
        date +%s
}

function __bashistant_print_info() {
        local INFO="${BASHISTANT_INFO_PADDING}$1"

        local INFO_NOCOLOR="$(echo -e "$INFO" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g")"

        if [ "
$BASHISTANT_INFO_ALIGN" = "right" ]; then
                echo -ne "
\033[${COLUMNS}C"
                echo -ne "
\033[${#INFO_NOCOLOR}D"
        fi
        if [ -n "
$BASHISTANT_COLOR_ENABLE" ] && [ $BASHISTANT_COLOR_ENABLE -eq 1 ]; then
                echo -e "
${INFO}"
        else
                echo -e "
${INFO_NOCOLOR}"
        fi
}

function __bashistant_set_color() {
        echo "
\033[${1}m"
}

function __bashistant_show_info() {
        local ELAPSED="
$1"

        if [ $ELAPSED -ge $BASHISTANT_SHOW_TIME ]; then
                local SHOWTIME="
"
                for elem in $BASHISTANT_INFO_FORMAT; do
                        SHOWTIME+="
"
                        case $elem in
                                "
%NOW")
                                        local NOW="
$(date +"${BASHISTANT_NOW_FORMAT}")"
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_NOW_COLOR)${NOW}\033[0m"
                                        ;;
                                "
%ELAPSED")
                                        local ELTIME
                                        if [ $ELAPSED -eq 0 ]; then
                                                ELTIME="
0s"
                                        else
                                                ELTIME="
$((ELAPSED/86400))d $(date -ud@"$ELAPSED" "+%Hh %Mm %Ss")"
                                                ELTIME="
$(echo " $ELTIME" | sed -e 's/[[:space:]]00\?[dhms]//g' -e 's/^[[:space:]]*//')"
                                        fi
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_ELAPSED_COLOR)$ELTIME\033[0m"
                                        ;;
                                "
%CPULOAD")
                                        local LOAD="
$(cat /proc/loadavg | awk '{ print $1 }')"
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_LOAD_COLOR)$LOAD\033[0m"
                                        ;;
                                *)
                                        SHOWTIME+=$elem
                        esac
                done

                __bashistant_print_info "
$SHOWTIME"
        fi

}

function __bashistant_log_info() {
        local ELAPSED=$1
        local COMMAND="
$2"

        logger -t "
Bashistant" -i --id=$$ "($(id -un)) Time: ${ELAPSED}sec Command: $COMMAND"
}

function __bashistant_desktop_notification() {
        local COMAND="
$2"
        local ELAPSED="
$1"
        local MSG="
$3"
        if [ -z "
$MSG" ]; then
                MSG="
Comando finalizado: "$COMMAND" en $ELAPSED segundos"
        fi
        notify-send "
$MSG"
}

function __bashistant_zenity_notification() {
        local COMAND="
$2"
        local ELAPSED="
$1"
        local MSG="
$3"

        if [ -z "
$MSG" ]; then
                MSG="
Comando finalizado: "$COMMAND" en $ELAPSED segundos"
        fi
        echo zenity --info --width=300 --title="
Tarea finalizada" --text="$MSG"
}


function __bashistant_bringtofront_notification() {
        if [ -n "
$WINDOWID" ]; then
                xdotool windowactivate $WINDOWID
        fi
}


function __bashistant_notify_info() {
        local ELAPSED=$1
        local COMMAND="
$2"

        if [ $ELAPSED -ge $BASHISTANT_NOTIFY_TIME ]; then
                flock -x $BASHISTANT_LOCKFD
                NOTIFY="
$(cat $BASHISTANT_DIR/notify 2>/dev/null)"
                flock -u $BASHISTANT_LOCKFD
                rm -f "
$BASHISTANT_LOCK"
                NOTIFY="
${NOTIFY//%ELAPSED%/$ELAPSED}"
                NOTIFY="
${NOTIFY//%COMMAND%/$COMMAND}"
                NOTIFY="
${NOTIFY//%USER%/$(id -un)}"
                NOTIFY="
${NOTIFY//%HOSTNAME%/$(hostname)}"

                while read notifycommand; do
                        if [ -n "
$notifycommand" ]; then
                                declare -a "
ncommand=($notifycommand)"
                                case ${ncommand[0]} in
                                        "
@notify")
                                                __bashistant_desktop_notification "
$ELAPSED" "$COMMAND" "${ncommand[@]:1}"
                                                ;;
                                        "
@zenity")
                                                __bashistant_zenity_notification "
$ELAPSED" "$COMMAND" "${ncommamd[@]:1}"
                                                ;;
                                        "
@bringtofront")
                                                __bashistant_bringtofront_notification
                                                ;;
                                        *)
                                                "
${ncommand[@]}"
                                esac
                                unset ncommand
                        fi
                done <<< $NOTIFY

        fi
}

function notify() {
        local ARGUMENT="
$1"

        if [ -z "
$ARGUMENT" ]; then
                cat $BASHISTANT_DIR/notify 2>/dev/null
        else
                echo "
$ARGUMENT" > $BASHISTANT_DIR/notify
                echo "
Notificación definida con éxito"
        fi
}

function postcmd() {
    if [ "
${#COMMANDS[@]}" -gt 1 ]; then
                HISTORY=$(history 1)
                COMMAND="
${HISTORY:7}"
                if [ -z "
$_BASHISTANT_START" ]; then
                        # No start info
                        return
                fi
                local END=$(__bashistant_get_timestamp)
                local ELAPSED=$(($END - $_BASHISTANT_START))
                if [ -n "
$BASHISTANT_INFO_ENABLE" ] && [ $BASHISTANT_INFO_ENABLE -eq 1 ]; then
                        __bashistant_show_info "
$ELAPSED"
                fi

                if [ -n "
$BASHISTANT_LOG_ENABLE" ] && [ $BASHISTANT_INFO_ENABLE -eq 1 ]; then
                        __bashistant_log_info "
$ELAPSED" "$COMMAND"
                fi

            if [ -n "
$BASHISTANT_NOTIFY_ENABLE" ] && [ $BASHISTANT_NOTIFY_ENABLE -eq 1 ]; then
                        __bashistant_notify_info "
$ELAPSED" "$COMMAND"
                fi
fi;
    COMMANDS=();
    trap 'precmd' debug

}

function precmd() {
        if [ ${#COMMANDS[@]} -eq 0 ]; then
                _BASHISTANT_START=$(__bashistant_get_timestamp)
                #echo "
INICIA EJECUCIÓN: "$BASH_COMMAND
        fi
        COMMANDS+=("
$BASH_COMMAND");
}

readonly PROMPT_COMMAND="
postcmd"
trap 'precmd' debug

En principio el archivo lo llamé bashistant.sh (que viene de Bash Assistant, todo mezclado). Y quiero modificarlo un poco para integrarlo en los gscripts.

Activación

Para poder utilizar este script automáticamente en nuestras sesiones de terminal, podemos editar nuestro archivo ~/.bashrc y añadir la siguiente línea (cambiando la ruta del archivo por la adecuada en tu sistema):

1
source $HOME/gscripts/bashistant.sh

También podemos utilizar los archivo $HOME/.profile o /etc/profile. El último para instalar a nivel de sistema para todos los usuarios.
En principio se creará el directorio .bashistant en tu $HOME para almacenar información sobre las notificaciones, aunque en el futuro se utilizará para más cosas. La inicialización no es muy pesada. Aparte de crear el directorio mencionado anteriormente, obtenemos el ID del proceso emulador de terminal (konsole, xfce4-terminal, gnome-terminal…), y si estamos en un entorno gráfico, obtiene el ID de la ventana que lo gobierna, para resaltar la ventana cuando no nos encontramos visualizándola.

Log de comandos

Esta es la parte menos currada por el momento, se limita a llamar a logger con el comando una vez finalizado. Podemos ver un fragmento de /var/log/syslog aquí:

Dec 18 14:18:04 gaspy-ReDDraG0N Bashistant[25301]: (gaspy) Time: 0sec Command: cat pkey.pem
Dec 18 14:18:50 gaspy-ReDDraG0N Bashistant[25301]: message repeated 2 times: [ (malvado) Time: 4sec Command: rm -rf archivos_confidenciales]
Dec 18 14:43:48 gaspy-ReDDraG0N Bashistant[25301]: (gaspy) Time: 578sec Command: find -name ‘confidencial’
Dec 18 16:24:34 gaspy-ReDDraG0N Bashistant[10252]: (gaspy) Time: 0sec Command: pgrep -f apache

Y esto podría delatar al usuario malvado, que normalmente no debe tener permiso para editar logs ni nada parecido.

Este log podemos desactivarlo haciendo:

BASHISTANT_LOG_ENABLE=0

Si queremos pillar a alguien que ha ejecutado comandos malignos (que no es el cometido de este script), podríamos desactivar esta característica en el código.

Información tras la ejecución

En realidad esto lo encontré en el GitHub de Chuan Ji mientras buscaba información y me gustó la visualización que hacía tras cada comando. No le copié el código, como vemos, su proyecto tiene más precisión midiendo el tiempo. A mí, para Bash, no me interesaba tener demasiada precisión en ello. Pero además, quise completarlo y hacerlo algo más personalizable.

Para modificar el comportamiento de esta característica tenemos una serie de variables que podremos modificar desde nuestro terminal:

  • BASHISTANT_INFO_ENABLE=[0,1] : Activa o desactiva esta característica
  • BASHISTANT_SHOW_TIME=[n] : Número de segundos que debe tardar la tarea para mostrar esta línea. Podemos hacer que si un comando tarda demasiado poco no se muestre nada, o 0 si queremos que se muestre siempre.
  • BASHISTANT_NOW_FORMAT=”%d/%m/%Y %H:%M:%S”: Formato de fecha y hora (se usa el comando date.
  • BASHISTANT_NOW_COLOR=[color]: Código ANSI del color para mostrar la fecha y hora actuales.
  • BASHISTANT_INFO_ALIGN=[left|right] : Alineación del texto de información (izquierda o derecha).
  • BASHISTANT_INFO_PADDING=” “: Texto separador para que el recuadro no esté pegado al borde de la pantalla.
  • BASHISTANT_ELAPSED_COLOR=[color]: Código de color para el tiempo transcurrido en la ejecución.
  • BASHISTANT_LOAD_COLOR=[color]: Código de color para la carga del sistema.
  • BASHISTANT_INFO_FORMAT=”[ %NOW | %ELAPSED | %CPULOAD ]”: Formato por el que se muestra la información.

Después de unos días de uso se ha convertido en una buena herramienta sobre todo para determinar de un vistazo cuánto tiempo llevas trabajando en un servidor o necesitas saber cuánto ha tardado una tarea que has olvidado cronometrar. Sí, en ocasiones, lanzamos tareas como mysqldump, restauraciones de base de datos, instalaciones, orquestaciones de un sistema, etc. En definitiva, tareas que pueden llegar a tardar incluso varias horas y que, muchas veces te interesa saber cuánto tiempo han necesitado esas tareas, por ejemplo para hacer documentación. Pero cuando te acuerdas de que deberías cronometrarlo (bastaría con ejecutar un comando poniendo time delante), es cuando el proceso ha terminado, puede llevar ya una hora y no vas a cancelar el proceso a estas alturas… espero no ser el único al que le pasa esto 🙂

Configurar notificaciones

El archivo $HOME/.bashistant/notify lo podremos modificar con un editor de textos para introducir el comando que queremos que se ejecute para notificar la finalización de la orden de Bash. Igual que en el punto anterior, muchas veces puedo lanzar un comando que necesitará mucho tiempo para finalizar y me pongo a hacer otra cosa. Es en esos casos en los que olvido esa primera tarea que dejé haciéndose y puede pasar mucho tiempo hasta que me acuerdo de ella. Una solución rápida, sería ejecutar el comando así:

time comando_largo ; zenity --info --text=”El comando ha terminado”

Pero, como siempre, se me olvida hacer esto cuando voy a ejecutar la orden. Otra opción sería:

comando largo
^Z
fg ; zenity --info --text=”El comando ha terminado”

Es decir, una vez hemos lanzazdo el comando, pulsamos Control+Z para pausarlo y luego utilizamos fg para reanudarlo, haciendo que una vez reanudado se ejecute zenity para notificar su finalización. Aunque muchas veces no se pueden pausar los comandos sin cargarnos algo.

Así que este script, cuando termina la ejecución de la orden, mirará el archivo ~/.bashistant/notify para ver qué comando tiene que ejecutar. Lo que significa que, aunque la tarea esté en ejecución puedo modificar el contenido de ese archivo, que cuando el comando termine se leerá y se ejecutará lo que ahí diga. Para agilizar un poco más la definición de notificaciones podemos utilizar en el mismo comando de notificación las siguientes palabras clave:

  • %ELAPSED%: Para mostrar el tiempo empleado en segundos.
  • %COMMAND%: Para mostrar el comando que acaba de finalizar.
  • %USER%: Para mostrar el usuario que ha ejecutado el comando
  • %HOSTNAME%: Para mostrar el hostname del equipo

Por lo que el archivo ~/.bashistant/notify quedaría así:

1
zenity --info --text="El comando %COMMAND% ejecutado por %USER%@%HOSTNAME% ha finalizado en %ELAPSED% segundos."

Además, disponemos de algunos comandos rápidos como:

  • @notify : Para ejecutar notify-send
  • @zenity : Para generar un diálogo ded zenity
  • @bringtofront : Para traer al primer plano el terminal

Que permiten que ~/.bashistant/notify contenga:

1
@notify

Para realizar la función.

También podemos utilizar el comando notify para definir el comando de notificación. Por lo que, podemos ejecutar un comando muy largo en un terminal, y luego desde otro sitio (con el mismo usuario), hacer:

notify @bringtofront

Así, cuando termine el primer comando (el que nos lleva mucho tiempo), se traerá a primer plano la ventana de terminal desde la que se disparó.

Por otro lado, como sería muy pesado estar todo el rato viendo notificaciones de comandos terminados, ya que un simple cd o ls dispararía la notificación tenemos las variables:

  • BASHISTANT_NOTIFY_ENABLE=[0|1]: Que podemos usar para activar o desactivar la característica.
  • BASHISTANT_NOTIFY_TIME=[n]: Con la que podemos decir el tiempo mínimo para que una tarea se notifique. Por defecto vale 10, quiere decir que si una tarea lleva menos de 10 segundos, no disparará la notificación.

Más posibilidades

En el comando de notificaciones podríamos, por ejemplo, reproducir un sonido, o una voz, o generar una línea con cURL que dispare un webhook de slack, gitlab o cualquier otra aplicación. Podemos programar el envío de un e-mail. O incluso podemos ejecutar varios comandos, uno por línea.

Si miráis el código podéis ver que hay ciertas cosas que no se usan, por ahora, como la captura de los comandos justo antes de que se ejecuten, o la variable MYPIDS reservada para una futura sorpresa en la siguiente versión. Eso sí, estoy abierto a sugerencias y, por supuesto, os invito a contribuir con este pequeño script.

Foto principal: unsplash-logoMathew Schwartz

The post Notifica, logea y enriquece tu experiencia de trabajo en Bash con este script appeared first on Poesía Binaria.

» Leer más, comentarios, etc...

Koalite

TypeScript: varianza y solidez

enero 21, 2019 05:06

Hace un año escribía sobre las diferencias entre los sistemas de tipos nominales y los sistemas de tipos estructurales. También explicaba por qué el tipado estructural que utiliza Typescript puede suponer un problema a la hora de mantener invariantes en el modelo de datos de una aplicación.

En este post quiero retomar el tema centrándome en las peculiaridades que presenta el sistema de tipos de Typescript a la hora de decidir si el tipo de dos funciones es estructuralmente compatible y las implicaciones que ello tiene. Por el camino pasaremos de puntillas por conceptos como la varianza de tipos y la solidez (soundness) de un sistema de tipos.

Tipado nominal y tipado estructural

No me voy a extender en este punto porque ya lo traté con más detalle, pero vamos a recordar las características básicas de cada sistema de tipos.

En un sistema de tipos nominal sólo podemos hacer asignaciones entre referencias del mismo tipo o que implementen un tipo común (una clase base o interfaz, por ejemplo). El nombre define la identidad del tipo, y si tenemos dos tipos diferentes, aunque tengan exactamente la misma estructura interna, no son compatibles.

En los sistemas de tipos estructural es la estructura del tipo la que define la compatibilidad. Eso permite que podamos realizar asignaciones entre cosas que, aunque a priori están definidas con tipos diferentes, tengan igual estructura (o al menos parecida).

Compatibilidad de funciones en TypeScript

Ya he mencionado antes que TypeScript utiliza tipado estructural para decidir la compatibilidad entre tipos pero, ¿cuándo se considera que dos funciones son compatibles?

Como era de esperar, esto va a depender tanto del tipo del valor de retorno de la función y los parámetros, como del número de los mismos.

Empezando por el valor de retorno, podemos tener casos como esto:

type ReturnsVoid = (x: string) => void;
type ReturnsBoolean = (x: string) => boolean;
type ReturnsBooleanOrString = (x: string) => boolean | string;

let f1: ReturnsVoid;
let f2: ReturnsBoolean;
let f3: ReturnsBooleanOrString;

// OK: f1 no devuelve nada, nos da igual que f2 devuelva algo
f1 = f2;

// Error: f1 no devuelve nada pero f2 debe devolver boolean
f2 = f1; 

// OK: f2 devuelve boolean, que es válido para boolean|string
f3 = f2;

// Error: f3 puede devolver string, que no es válido para boolean
f2 = f3;

Podemos asignar una función f1 a otra f2 si el tipo de retorno de f1 es más específico (es un subtipo) que el tipo de retorno de f2. Si lo piensas un poco, tiene sentido, porque los consumidores de f2 están preparados para trabajar con todos sus posibles valores de retorno, y los posibles valores de retorno de f1 son un subconjunto de los de f2. Técnicamente, esto se conoce como covarianza, y podemos decir que las funciones con covariantes con respecto al tipo de retorno.

¿Qué ocurre con los parámetros? Pues cabría esperar que más o menos lo contrario, pero no:

type AcceptsBoolean = (x: boolean) => void;
type AcceptsBooleanOrString = (x: boolean | string) => void;

let f1: AcceptsBoolean;
let f2: AcceptsBooleanOrString;

// OK: f2 trata boolean y string, y a través de f1 sólo llegará boolean
f1 = f2;

// OK: ¿Cómo? ¿Esto funciona?
f2 = f1;
// No, explota en tiempo de ejecución. Pero TypeScript te deja.
f2("hola");

En los lenguajes de programación sensatos otros lenguajes de programación se permite la primera asignación porque el tipo del parámetro de f1 es más restrictivo que el de f2, así que cuando se vaya a usar a través de la referencia f1 todos los posibles valores que se pasen serán válidos para f2. El nombre técnico de eso es contravarianza y diríamos que las funciones son contravariantes con respecto al tipo de sus parámetros.

Sin embargo, TypeScript permite también el segundo caso, que podría dar lugar a un error en tiempo de ejecución si usamos la referencia f2 para invocar f1 pasándole como parámetro un string. Eso se conoce como bivarianza y, aunque tiene su razón de ser en TypeScript, la verdad es que no me convence mucho. Por suerte en versiones recientes de TypeScript es posible desactivar este comportamiento con el parámetro strictFunctionTypes.

Todo esto de la covarianza y contravarianza tiene su definición formal y va un poco más allá de lo que hemos visto aquí.

Si pasamos a analizar la compatibilidad de funciones desde el punto de vista del número de argumentos, encontramos los siguientes casos:

type OneArg = (x: number) => void;
type TwoArgs = (x: number, y: number) => void;
type Optional = (x: number, y?: number) => void;
type Rest = (x: number, ...y: number[]) => void;

let f1: OneArg;
let f2: TwoArgs;
let f3: Optional;
let f4: Rest;

// OK: Al invocar a través de f2, f1 recibirá 
//     un segundo parámetro que puede ignorar
f2 = f1;

// Error: Al invocar a través de f1, f2 sólo recibirá 
//        un parámetro de los 2 que requiere
f1 = f2;

// OK: Parámetros opcionales y rest se tratan igual
f3 = f4;
f4 = f3;

// OK: ¿Cómo? ¿Esto funciona?
f3 = f2;
// No, esto puede explorar en tiempo de ejecución
// porque está invocando f2 con un sólo parámetro
f3(2) 

Una función f1 que recibe menos parámetros, puede ser asignada a otra función f2 que recibe más. Esto tiene (cierto) sentido, porque cuando la invoquemos a través de la referencia f2 se le pasarán parámetros adicionales que la función ignorará. El caso opuesto, por suerte, no está permitido, porque nos faltarían parámetros en la invocación.

Desgraciadamente, en cuanto metemos parámetros opcionales la cosa vuelve a desmadrarse y se permiten asignaciones que pueden provocar problemas en tiempo de ejecución.

Las consecuencias de todo esto

Todo esto puede parecer muy rebuscado y que nunca pasa en la vida real, pero en cuanto empiezas a pasar funciones como parámetros de otras funciones (algo muy habitual en código javascript/typescript), es fácil que te salpique:

// Partimos de esta función...
function printNumbers(
  numbers: number[], 
  printer: (n: number) => void) {
    numbers.forEach(printer);
}

// ...que podríamos usar de esta forma
printNumbers([1, 2, 3], n => console.log(n));

// Típica función que, tal vez, viola el SRP
function printAndKill(n: number, killKitten?: boolean): void {
    console.log(n);
    if (killKitten) {
        // Matar gatito
    }
}

// Ops. 2 animalillos menos.
printNumbers([1, 2, 3], printAndKill)

Si analizamos las asignaciones de funciones que se están haciendo, veremos que la “flexibilidad” que proporciona TypeScript quizá sea excesiva:

Primero se permite pasar como parámetro printer de la función printNumbers la función printAndKill. Eso es posible porque el segundo parámetro de printAndKill es opcional, así que se supone que es capaz de trabajar con sólo un number, que es lo que se le pasará a printer.

Hasta aquí, vamos bien.

Después la función printer, que recibe un number, nos permite asignarla al parámetro del método forEach de Array<number>, que tiene como tipo (value: number, index: number, array: number[]) => void).

Esto es así porque una función que recibe menos parámetros (printer) es asignable a una que recibe más (la callback de forEach). Total, los que le pasen extra los ignora y ya está, ¿no?

El problema es que printer no sólo recibe un parámetro number, sino que internamente almacena una referencia a printAndKill, que recibe, opcionalmente un segundo parámetro. Ops. Si unimos esto al tipado débil de javascript, cuando se invoque printAndKill se hará la coerción de index a boolean, por lo que inadvertidamente mataremos gatitos en cuanto index != 0.

Solidez en sistemas de tipos

Esto que hemos visto no es algo exclusivo del sistema de tipos de TypeScript, y hay muchos lenguajes (entre ellos todos los más populares) que tienen problemas parecidos. Por ejemplo, en C# podemos encontrar un caso similar con la covarianza de arrays, que permite hacer cosas como ésta:

Dog[] dogs = new [] { new Dog() };

// OK: los arrays con covariantes en C#
Animal[] animals = dogs;

// Error en tiempo de ejecución
animals[0] = new Cat(); 

A esta propiedad de los sistemas de tipos se le llama solidez (soundness). Se dice que un sistema de tipos es sólido (sound) si no permite considerar válidos programas que luego darán errores en tipo de ejecución. Al resto, se les llama frágiles (unsound).

Esto puede resultar un poco chocante. Se supone que una de las ventajas (¿la principal?) de los sistemas de tipado estático es que permite comprobar en tiempo de compilación la validez del programa (desde el punto de vista de compatibilidad de tipos). Si a veces da por buenos programas inválidos, ¿qué sentido tiene?

La realidad es un poco más complicada y, al igual que los sistemas de tipado dinámico o gradual tienen su utilidad, tener sistemas frágiles también tiene sus ventajas. Por una parte puede simplificar la parte de validación de tipos, mejorando el rendimiento de los compiladores. Por otra, hay escenarios en los que gracias a la falta de solidez se simplifica mucho el código a escribir.

Por ejemplo, si en el caso anterior de C# no existiera covarianza de arrays, no se podría hacer cosas como:

abstract class Animal {
  abstract void Move()
}
class Dog : Animal {...}
class Cat: Animal {...}

void MoveAll(Animal[] animals) {
  foreach (var animal in animals)
    animal.Move();
}

MoveAll(new Dog[] { new Dog(), new Dog() });
MoveAll(new Cat[] { new Cat(), new Cat() });

En ese caso se podría solucionar haciendo los arrays invariantes y haciendo que MoveAll recibiera un tipo covariante (por ejemplo IEnumerable), pero hay veces que compensa sacrificar la solidez para facilitar este tipo de patrones de uso.

Para el caso de TypeScript, en esta discusión de GitHub podéis encontrar varios argumentos interesantes sobre el tema.

Posts relacionados:

  1. Mantenimiento de invariantes en TypeScript
  2. Extender tipos existentes en TypeScript
  3. Test builders en TypeScript

» Leer más, comentarios, etc...

proyectos Ágiles

Cómo desescalar una organización – Un caso real

enero 15, 2019 06:00

En esta presentación se muestra un caso real de estrategia de transformación Agile INTEGRAL donde una de las premisas es el DESESCALADO. Adicionalmente, se identifican cambios clave a realizar como, por ejemplo, pasar de trabajar en formato “proyecto” a ser guiados por objetivos estratégicos, cómo estructurar una organización alrededor de estos objetivos introduciendo a la vez los necesarios cambios arquitectónicos para hacerlo viable técnicamente y cómo hacer un rediseño cultural para conseguir ownership del “sistema” por parte de la gente.

Para avanzar en toda esta transformación se necesita accionar una gran cantidad de áreas y muchas acciones se realimentan mutuamente. En esta presentación verás:

  • Una estrategia de gestión del cambio específica para Agile y un roadmap real, por qué estados se puede pasar, qué criterios utilizar para priorizar todo el trabajo a realizar.
  • Cuáles son los factores clave para avanzar.
  • Cuáles han sido los valores “no escritos” (o que hay que escribir 🙂 ) para ayudar a la transformación.
  • Qué hacer para que la transformación sea “de todos” (compartida, inclusiva y colaborativa).
  • Cómo hacer tracción desde un departamento online al resto de la empresa.
  • Qué errores puedes cometer y cómo evitarlos.
  • Cómo hacer visibles los resultados, cómo conseguir engagement y buenas conversaciones a todos los niveles (Dirección, técnicos, etc,).

Presentaciones relacionadas

Artículos relacionados:

Blogs relacionados

» Leer más, comentarios, etc...

Poesía Binaria

Cómo utilizar PHP desde contenedores docker tanto de forma local como en producción

enero 14, 2019 09:25

Una de las medidas más importantes que debemos tomar a la hora de realizar proyectos de programación, sobre todo en equipo, es asegurarte de que todos tienen el mismo entorno de trabajo. Si nos referimos a un entorno de aplicaciones en PHP, todos los miembros involucrados deberán tener la misma versión de PHP, las mismas extensiones y la misma configuración del sistema. Además, esas mismas configuraciones deberían ser las mismas en los entornos de test y producción, aunque en producción quitemos las herramientas de debug o depuración. Así, cuando hay un problema, podemos evitar en la medida de lo posible el famoso: “En mi ordenador funciona” cuando al subir cambios al servidor, o al probarlos un compañero, no funcionan.

Tenemos varias opciones al respecto. Podemos utilizar máquinas virtuales con soluciones como Vagrant. Otra opción es utilizar Docker para ejecutar PHP. Incluso podemos ejecutar PHP desde nuestra terminal a través de docker con aplicaciones como composer, o artisan de Laravel.

Dockerfile

Vamos a basarnos en las imágenes oficiales de php, en este caso php7.2. Y vamos a instalar por defecto extensiones como xml, zip, curl, gettext o mcrypt (esta última debemos instalarla desde pecl.
En los contenedores vamos a vincular /var/www con un directorio local, que puede estar por ejemplo, en la $HOME del usuario actual, donde podremos tener nuestros desarrollos. Y, por otro lado, la configuración de PHP también la vincularemos con un directorio fuera del contenedor, por si tenemos que hacer cambios en algún momento. En teoría no deberíamos permitir esto, la configuración debería ser siempre fija… pero ya nos conocemos y siempre surge algo, lo mismo tenemos que elevar la memoria en algún momento, cambiar alguna directiva o alguna configuración extra.

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
FROM php:7.2-fpm
ARG HOSTUID
ENV BUILD_DEPS="autoconf file gcc g++ libc-dev make pkg-config re2c libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev libssl-dev libc-client-dev libkrb5-dev zlib1g-dev libicu-dev libldap-dev libxml2-dev libxslt-dev libcurl4-openssl-dev libpq-dev libsqlite3-dev" \
    ETC_DIR="/usr/local/etc" \
    ETC_BACKUP_DIR="/usr/local/etc_backup"

RUN apt-get update && apt-get install -y less \
    procps \
    git \
    && pecl install redis \
    && pecl install xdebug \
    && docker-php-ext-enable redis xdebug \
    && apt-get install -y $BUILD_DEPS \
    && docker-php-ext-install -j$(nproc) iconv \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
    && docker-php-ext-install -j$(nproc) imap \
    && docker-php-ext-install -j$(nproc) bcmath \
    && docker-php-ext-install -j$(nproc) calendar \
    && docker-php-ext-install -j$(nproc) exif \
    && docker-php-ext-install -j$(nproc) fileinfo \
    && docker-php-ext-install -j$(nproc) ftp \
    && docker-php-ext-install -j$(nproc) gettext \
    && docker-php-ext-install -j$(nproc) hash \
    && docker-php-ext-install -j$(nproc) intl \
    && docker-php-ext-install -j$(nproc) json \
    && docker-php-ext-install -j$(nproc) ldap \
    && docker-php-ext-install -j$(nproc) sysvshm \
    && docker-php-ext-install -j$(nproc) sysvsem \
    && docker-php-ext-install -j$(nproc) xml \
    && docker-php-ext-install -j$(nproc) zip \
    && docker-php-ext-install -j$(nproc) xsl \
    && docker-php-ext-install -j$(nproc) phar \
    && docker-php-ext-install -j$(nproc) ctype \
    && docker-php-ext-install -j$(nproc) curl \
    && docker-php-ext-install -j$(nproc) dom \
    && docker-php-ext-install -j$(nproc) soap \
    && docker-php-ext-install -j$(nproc) mbstring \
    && docker-php-ext-install -j$(nproc) posix \
    && docker-php-ext-install -j$(nproc) pdo_pgsql \
    && docker-php-ext-install -j$(nproc) pdo_sqlite \
    && docker-php-ext-install -j$(nproc) pdo_mysql \
    && yes | pecl install "channel://pecl.php.net/mcrypt-1.0.1" \
    && { \
    echo 'extension=mcrypt.so'; \
    } > $PHP_INI_DIR/conf.d/pecl-mcrypt.ini \
    && echo "Fin de instalaciones"
COPY docker-entry.sh /usr/local/binx

RUN mv $ETC_DIR $ETC_BACKUP_DIR \
    && chmod +x /usr/local/bin/docker-entry.sh \
    && rm /etc/localtime

RUN useradd -s /bin/bash -d /var/www -u $HOSTUID user

ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
CMD ["php-fpm"]

También tendremos un archivo docker-entry.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
echo "Iniciando contenedor"

VERSION="7.2"
CONFFILE=/etc/php/$VERSION/fpm/php-fpm.conf
DOCKERIP=$(hostname --ip-address)

if [ $(ls $ETC_DIR | wc -l) -eq 0 ]; then
        echo "Copiando configuración por defecto"
        cp -r "$ETC_BACKUP_DIR"/* "$ETC_DIR"
fi

/usr/local/bin/docker-php-entrypoint $@

Para construir la máquina podemos utilizar esto:

docker build -t myphp7.2-fpm --build-arg HOSTUID=”$(id -u)” --cpuset-cpus=”0-7″ .

Utilizo cpuset-cpus para delimitar los núcleos que vamos a utilizar para compilar los módulos. Esto puede tardar un poco y, si tenemos varios núcleos, puede interesarnos utilizar uno o dos, y mientras se construye PHP, utilizar el ordenador para navegar por Internet o algo así. Yo suelo crear un archivo build.sh con esa misma línea de antes.

Ahora, tendremos unos argumentos a la hora de lanzar el contenedor (run.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
readonly SCRIPTPATH="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
readonly WWWPATH="
$HOME/www"

GATEWAY="
"
if [ -n "
$(which dnsmasq)" ]; then
    if [ -z "
$(pidof dnsmasq)" ]; then
        sudo dnsmasq --bind-interfaces
    fi
    GATEWAY="
--dns $(ip addr show docker0 | grep -Po 'inet \K[\d.]+')"
fi

pushd $SCRIPTPATH > /dev/null
docker run --rm --name myphp7.2-fpm -v /etc/localtime:/etc/localtime:ro -v $WWWPATH:/var/www:rw -v $(pwd)/conf:/usr/local/etc/:rw $GATEWAY --user www-data --cpuset-cpus="
7" -d myphp7.2-fpm

Este archivo podremos reescribirlo dependiendo de nuestra configuración local. En mi ordenador, utilizo dnsmasq como dns en la máquina host, de forma que si modifico mi /etc/hosts, pueda acceder a dichos nombres desde mi contenedor PHP. Además, es conveniente editar en este archivo la variable WWWPATH donde estableceremos la ruta base desde la que tendremos todos nuestros archivos PHP, a partir de la que serviremos con FPM los archivos.

Configurando un servidor web

Este PHP con FPM debemos configurarlo en un servidor web, para ello utilizaremos proxy_fcgi dejando el VirtualHost más o menos así (no he puesto configuración de SSL porque estoy en local, aunque también podríamos configurarla):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<VirtualHost *:80>
    ServerName prueba_de_mi_web.local

    ServerAdmin webmaster@localhost
    Define webpath /prueba_de_mi_web.com
    DocumentRoot /home/gaspy/www/${webpath}
    <Directory /home/gaspy/www/${webpath}/>
            Options +FollowSymLinks
        AllowOverride all
    </Directory>

    <IfModule proxy_fcgi_module>
         ProxyPassMatch "^/(.*\.ph(p[3457]?|t|tml))$" "fcgi://myphp7.2-fpm.docker.local:9000/var/www/${webpath}/$1"
         DirectoryIndex index.html index.php
    </IfModule>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Mi $HOME, en mi ordenador es /home/gaspy/ y en www almaceno todo el código de aplicaciones web. En mi /etc/hosts he hecho una entrada que apunta a la IP del contenedor. Para ello puedo utilizar este script que actualiza /etc/hosts con los dockers que hay en ejecución en este momento.

Ejecución desde línea de comandos

Una operación que realizo casi a diario en PHP es la ejecución de scripts desde la línea de comandos. Ya sea por una operación que se realiza en segundo plano, o ejecutar composer, o comandos propios de frameworks o plataformas como artisan de Laravel, occ de Owncloud, yii de su framework homónimo, etc.
Pero claro, para ejecutar los scripts, tengo que hacerlo desde dentro del contenedor, muchas veces con el usuario local y no como root, y generalmente los archivos a ejecutar se encontrarán en mi directorio local, pero dentro del contenedor estarán en /var/www. Además, tenemos que tener en cuenta que muchas veces ejecutaré código php de forma interactiva, es decir, ejecutando php y escribiendo el código, y otras veces haré algo así:

1
2
<?php
echo "Hola mundo!";

Para ello, tengo un script que ejecutará php (php7.sh):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash
readonly SCRIPTPATH="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"

CURRENT_DIR="
$(pwd)/"
BASE_DIR="
$HOME/www/"
DOCKER_DIR="
/var/www/"
CONTAINER_NAME="
myphp7.2-fpm"

TRANSLATED_DIR="
${CURRENT_DIR/$BASE_DIR/$DOCKER_DIR}"

if [ -z "
$(docker ps | grep $CONTAINER_NAME)" ]; then
    $SCRIPTPATH/run.sh
fi

if [ "
$1" == "-u" ]; then
    shift
        NUID=$(id -u "$1")
    shift
elif [ "
$1" == "-l" ]; then
    shift
    NUID=$UID
fi

if [ -n "
$1" ]; then
    set -- "
${1/$BASE_DIR/$DOCKER_DIR}" "${@:2}"
    QUOTED_ARGS="
$(printf " %q" "$@")"
fi

if [ -n "
$NUID" ]; then
    USER="
$(getent passwd 1000 | cut -f1 -d:)"
    DIR="
${DOCKER_DIR}${USER}"
    if [ ! -d "
${BASE_DIR}$USER" ]; then
        docker exec -u root -i myphp7.2-fpm bash -c "
mkdir "$DIR"; chown $NUID:$NUID "$DIR""
    fi
    docker exec -u "
$NUID:$NUID" -i myphp7.2-fpm bash -c "HOME="$DIR"; cd $TRANSLATED_DIR 2>/dev/null; exec php ${QUOTED_ARGS}"
else
    docker exec -i myphp7.2-fpm bash -c "
cd $TRANSLATED_DIR 2>/dev/null; exec php ${QUOTED_ARGS}"
fi

A este archivo, le podemos crear un enlace dentro de /usr/local/bin/ llamado php para que podamos ejecutarlo desde cualquier sitio:

sudo ln -s $(pwd)/php7.sh /usr/local/bin/php

El script, lo que hace primero es averiguar el directorio actual desde donde ejecutas el script, y transformará dentro de esa cadena la ruta $BASE_DIR (donde están los archivos php en mi ordenador) por la ruta $DOCKER_DIR (donde están los archivos php en el contenedor). De esta forma si, fuera de docker ejecutamos:

php archivo.php

Dicho archivo se buscará en el directorio correspondiente dentro del contenedor. Eso sí, fuera de $BASE_DIR no podremos ejecutar archivos. Adicionalmente podremos ejecutar:

php -u www-data archivo.php

Para ejecutar el archivo.php como el usuario www-data o también
php -l archivo.php

Si queremos ejecutar el archivo php como el usuario actual. Además, este script se encarga de lanzar la máquina docker (con run.sh) si no está en ejecución actualmente.

composer y otros scripts parecidos

Composer utilizará el script anterior de php para ejecutarse. Aunque podemos tener un problema con la salida en color. Así que podemos imponerla. Por otro lado, queremos ejecutar composer como el usuario actual, en lugar del usuario www-data, ya que todo lo que instala será código y el código no debe poder ser sobreescrito por ese usuario (generalmente).

Así que podemos crear este script en /usr/local/composer:

1
2
#!/bin/bash
php -u "$(whoami)" /home/gaspy/www/composer --ansi $@

Algunas posibilidades

Siempre podemos meter la pata en la configuración, por un lado, si queremos recargar la configuración, tendremos que parar el contenedor y reiniciarlo:

docker stop myphp7.2-fpm
php

Como vemos, simplemente cargando php se iniciará el contenedor de nuevo. Además, si hemos metido la pata en la configuración, podemos eliminar los archivos del directorio conf y reiniciar el contenedor para que automáticamente se restaure la configuración por defecto.

También podemos incluir nuestra propia configuración en el Dockerfile para que todo nuestro equipo tenga los mismos archivos de configuración la primera vez nada más construir la máquina.
Foto principal: unsplash-logoLuca Bravo

The post Cómo utilizar PHP desde contenedores docker tanto de forma local como en producción appeared first on Poesía Binaria.

» Leer más, comentarios, etc...

Una sinfonía en C#

Delegate, predicate, Action, Func, métodos anónimos, explicados para mortales.

enero 13, 2019 07:16

No es un tema nuevo ni mucho menos,  sin embargo no siempre es del todo bien comprendido y su correcta comprensión es muy importante ya que muchas otras características del framework se apoyan en los delegados, así que vamos a hablar un poco sobre ellos.

Pero, ¿Qué es un delegado?

La respuesta corta es “un delegado es una referencia a un método”, pero no confundir, no es una referencia como un puntero sino que en realidad un delegado nos permite definir una firma para poder luego pasar un método. Vemos un ejemplo.

var testDelegates = new TestDelegates();
testDelegates.DoSomething();

En este simple código tenemos un objeto testDelegates que tiene un método DoSomething y que hace algo, imaginemos que ese método, por ejemplo, hace una llamada a un servicio que ocurre de manera asíncrona y termina después de un tiempo indeterminado. La llamada se realiza pero queremos que de alguna manera este método nos informe que la ejecución ha finalizado. Bien, una forma de hacerlo sería indicarle a DoSomething  que luego de finalizar su ejecución invoque a otro método y nosotros hagamos algo en consecuencia. Más o menos cómo cuando hacemos una llamada AJAX desde Javascript donde tenemos una forma de ser informados de la finalización de la ejecución de la llamada.

Delegado simple

Para hacer esto necesitamos indicar a DoSomething cuál es el método que queremos que sea invocado al final su ejecución (exitosa o no, en este caso eso no nos preocupa) .NET nos permite hacer esto pero nos obliga de definir siempre la firma del método a ser llamado, es decir, tenemos de alguna manera que decir en DoSomething que podemos informar el final de la ejecución a un método pero con cierta firma (que reciba ciertos parámetros y devuelva cierto tipo de datos) acá es donde entran los delegados en acción, justamente nos permiten definir la firma del método.

public class TestDelegates
{
    // declaración del delegador, sin valor de retorno (void) y sin parámetros
    public delegate void DelegadoSimple();
    public void DoSomething(DelegadoSimple delegado)
    {
        // does something
        // llamar al la función
        delegado();
    }
}

Los primero que hacemos es definir el delegado de modo de que sea visible, como dijimos antes esta será la firma que deberá tener el método que pasaremos como parámetro, entonces lo ponemos en la clase que lo utilizará con visibilidad pública.

Luego ponemos como parámetro el delegado que funciona como un tipo y nos permite tener la referencia a la función.

Luego dentro del código podemos invocar la función, en este caso solo agregando los paréntesis porque no recibe parámetros.

private static void Main(string[] args)
{
    var testDelegates = new TestDelegates();
    testDelegates.DoSomething(FuncionSimple);
}

private static void FuncionSimple()
{
    // hacer algo
}

En cuando a la llamada a DoSomething simplemente agregamos el parámetro que es una función igual que el delegado, que no retorna valor (es void) y no recibe parámetros, notemos que no tiene parámetros porque no la invocamos solo pasamos una referencia.

Métodos anónimos

Qué pasaría si no quisiéramos crear una función con nombre y todo para que sea invocada. En este caso podemos crearla inline, de este modo:

private static void Main(string[] args)
{
    var testDelegates = new TestDelegates();
    // en este caso pasamos un método anónimo
    testDelegates.DoSomething(()=>
    {
        //hacer algo
    });
}

Lo que hicimos es declarar nuestro método en el mismo sitio que invocamos a DoSomething, vamos a analizar la sintaxis.

testDelegates.DoSomething(()=>
{
    //hacer algo
});

Primero tenemos que tenemos en cuenta que es necesario cumplir con el delegado, con lo cual el método anónimo no tiene valor de retorno y no recibe parámetros. entonces lo primero que vemos es que hay dos paréntesis vacíos, justamente porque no recibe parámetros y luego la “flecha” que indica que a continuación viene el cuerpo el método ()=>

Después simplemente tenemos las llaves que limitan el cuerpo del método anónimo y si tuviéramos código estaría por ahí, como el método no retorna valor no hay un return.

Es importante destacar que al ser anónimo (el método no tiene nombre) no puede ser invocado desde otra parte.

Imaginemos que lo que queremos hacer es escribir en la consola, el código quedaría así:

testDelegates.DoSomething(() =>
{
    Console.WriteLine("Ejecución finalizada");
});

Y qué ganamos creando un método anónimos? bien, lo principal es que es posible que no usemos ese método para nada más que para ser invocado por DoSomething, por otro lado tenemos visibilidad de las variables dentro del método donde declaramos el método anónimo, por ejemplo podemos hacer esto:

var testDelegates = new TestDelegates();
var variablePrivada = "valor original";
            // en este caso pasamos un método anónimo
testDelegates.DoSomething(() =>
{
    Console.WriteLine("Ejecución finalizada");
    Console.WriteLine($"El valor de la variable 'variablePrivada' es {variablePrivada}");
    variablePrivada = "valor nuevo";
    Console.WriteLine($"El nuevo valor de la variable 'variablePrivada' es {variablePrivada}");
});

No solamente podemos leer una variable privada del método donde declaramos el método anónimo sino que también podemos modificarla, algo similar a un closure.

Métodos anónimos con parámetros

public delegate void DelegadoSimple(int milisegundos);
public void DoSomething(DelegadoSimple delegado)
{
    // does something
    // llamar al la función
    delegado(20);
}

Primero cambiamos la definición del método anónimo para que reciba parámetros, en este caso el tiempo que demoró la ejecución de nuestro método DoSomething, esta vez cuando lo invocamos le pasamos el valor.

private static void Main(string[] args)
{
    var testDelegates = new TestDelegates();
    testDelegates.DoSomething(FuncionSimple);
}

private static void FuncionSimple(int milisegundos)
{
    Console.WriteLine("Ejecución finalizada");
    Console.WriteLine($"La ejecución tomó {milisegundos} milisegundos");
}

Modificamos el método para que reciba el parámetro y al ser ejecutada lo mostramos en pantalla, si lo hiciéramos anónimos sería así:

private static void Main(string[] args)
{
    var testDelegates = new TestDelegates();
    testDelegates.DoSomething((int milisegundos) =>
    {
        Console.WriteLine("Ejecución finalizada");
        Console.WriteLine($"La ejecución tomó {milisegundos} milisegundos");
    });
}

en este caso agregamos en parámetro dentro de los paréntesis.

¿Y qué pasa si tenemos que devolver valores en nuestros delegados?

Por supuesto que podemos tener un delegado que retorne un valor de cualquier tipo, un ejemplo sería el siguiente:

public delegate string DelegadoSimple(int milisegundos);
public void DoSomething(DelegadoSimple delegado)
{
    // does something
    // llamar al la función
    Console.Write(delegado(20));
}

Ahora cambiamos la declaración del delegado para que retorne un string y al invocar el delegado lo imprimimos, en código de la otra parte sería así:

testDelegates.DoSomething((int milisegundos) =>
{
    Console.WriteLine("Ejecución finalizada");
    return $"La ejecución tomó {milisegundos} milisegundos";
});

Muy sencillo.

Delegados predefinidos.

Existen algunos delegados predefinidos que nos permiten evitar declarar todo el tiempo los propios, lo más comunes son:

  • Predicate
  • Action
  • Func

El predicate se usa en muchos métodos de filtrado de colecciones, por ejemplo en la lista genérica:

public T Find(Predicate match)

Y si vemos la definición de predicate podemos comprender bien de qué se trata

public delegate bool Predicate(T obj)

Un predicate sirve para proveer al método Find de una filtro personalizado, es decir, definir nosotros cómo queremos filtrar los datos, vamos a ver el código interno del método Find

public T Find(Predicate match) {
	if( match == null) {
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
	}
	Contract.EndContractBlock();

	for(int i = 0 ; i < _size; i++) {
		if(match(_items[i])) {
			return _items[i];
		}
	}
	return default(T);
}

El predicate es la variable match y vemos que lo que hace este método Find es recorrer todos los elementos de la lista y si match es true, retorna el elemento que coincide. Esto nos demuestra la utilidad de los delegados, podemos nosotros definir el criterio de búsqueda.

Existen muchos otros métodos en la clase List<T> que aceptan un predicate y en otras clases también.

Action y Func

Action y Func son más simple, si miramos su definición nos damos cuenta para qué sirven

public delegate void Action();
public delegate void Action(T obj);
public delegate TResult Func();
public delegate TResult Func(T arg);

En el caso de Action es una delegado pre-definido que nos permite definir “acciones” es decir, métodos que no devuelven un valor, el ejemplo del principio lo podríamos haber hecho con Action.

public void DoSomething(Action delegado)
{
    delegado();
}

Lo mismo que si quisiéramos pasar uno o varios parámetros

public void DoSomething(Action delegado)
{
    delegado(20);
}

Existen muchas sobre cargas genéricas para poder pasar hasta 16 parámetros.

En el caso de Func es como un Action pero que devuelve un valor, entonces nuestro código final usando un Func quedaría así:

public void DoSomething(Func delegado)
{
    Console.WriteLine(delegado(20));
}

Simplemente Action y Func no son más que delegados pre-definidos que nos ahorran crear los nuestros en la mayor parte de los casos.

Es importante comprender bien los delegados porque están por todas partes, sobre todo en Linq, todo lo que sea asíncrono como HttpClient o lo relacionado con Threads.

Nos leemos.

» Leer más, comentarios, etc...

Fixed Buffer

Reconociendo a personas mediante Azure FaceAPI

enero 12, 2019 12:40

Azure

Han pasado ya las navidades, y toca la vuelta al trabajo, la vuelta a los viajes… y gracias a mi último viaje estuve cenando con un amigo, y me contó un proyecto que se trae entre manos, el cual requería del reconocimiento facial como parte de la identificación inequívoca de usuarios. En su momento, leí sobre los Cognitive Services de Azure, pero nunca me había aventurado a probarlos, y bueno, ya sabéis que soy curioso, así que en cuanto he tenido un momento, me he puesto a probarlo, ¡y la verdad es que ha sido realmente fácil! Vamos con ello:

Crear el servicio Azure FaceAPI

Lo primero que necesitamos, es tener una cuenta en Azure, no es necesario que sea de pago (aunque para confirmar la cuenta nos piden una tarjeta de crédito, no se efectúa ningún cargo).

Una vez que la tenemos, vamos a crear el servicio:

CrearServicio

Para ello, con eso, nos muestra una nueva ventana donde poner nuestros datos:

NombreServicio

En ella, basta con que le indiquemos el nombre, que suscripción queremos utilizar, donde queremos que este el servidor, el plan de precios (OJO!!, yo utilizo “S0” porque estoy haciendo otra prueba más en profundidad y tengo el “F0” ocupado, pero el gratuito es “F0”) y por último nos pide el grupo de recursos al que queremos que pertenezca (si no tenemos ningún grupo, lo podemos crear fácilmente con el botón “Crear nuevo”).

Con este paso, pulsamos sobre “Crear”, y como podemos ver en el dashboard, ya tenemos nuestro servicio creado:

dashboard

Solo nos queda conocer la url que debemos utilizar, y crear las claves para poder usarla, para eso, basta con pulsar sobre el servicio para ver su configuración:

overview

He indicado en amarillo la url de endpoint que tendremos que indicarle a la API en nuestro programa, en mi caso es:
https://westeurope.api.cognitive.microsoft.com/face/v1.0

Justo debajo, tenemos el botón para gestionar nuestras claves de acceso, lo que lanza una ventana donde nos las muestra:

keys

Debemos apuntarlas (o volver luego y consultarlas) al igual que la url del endpoint, ya que son datos que le vamos a tener que pasar a la API desde nuestro programa.

Creando nuestro programa

Para poder consumir nuestro servicio Azure FaceAPI, vamos a utilizar una aplicación de formularios en .Net Framework, pero por supuesto, podemos utilizarlo en nuestro proyecto NetCore (pero esta vez, la librería cambia de nombre). Para ello, creamos nuestro proyecto de formularios, y añadimos el paquete:

PM->Install-Package Microsoft.ProjectOxford.Face -v 1.4.0

Si estuviésemos en NetCore, por ejemplo en nuestra web ASP, el paquete seria:

PM->Install-Package Microsoft.ProjectOxford.Face.DotNetCore -Version 1.1.0

Al ser un código extenso, pongo el enlace para descargarlo desde GitHub, y aquí solo vamos a ver las partes claves de la API:

 
var faceServiceClient = new FaceServiceClient("Tu Key", "Url de la API");

Crearemos una instancia del cliente de la API, que luego utilizaremos.

 
var groups = await faceServiceClient.ListPersonGroupsAsync();

Listamos los grupos que ya existan creador previamente.

 
await faceServiceClient.DeletePersonGroupAsync(group.PersonGroupId);

Borramos un grupo.

 
await faceServiceClient.CreatePersonGroupAsync(GroupGUID, "FixedBuffer");

Creamos un grupo.

 
var personResult = await faceServiceClient.CreatePersonAsync(GroupGUID, personName);

Creamos una persona dentro del grupo.

 
var persistFace = await faceServiceClient.AddPersonFaceInPersonGroupAsync(GroupGUID, personResult.PersonId, fStream, file.FullName);

Añadimos una foto de training a la persona.

 
await faceServiceClient.TrainPersonGroupAsync(GroupGUID);

Hacemos el entrenamiento del grupo.

 
var faces = await faceServiceClient.DetectAsync(fStream);

Detectamos las caras de la imagen.

 
var results = await faceServiceClient.IdentifyAsync(GroupGUID, faces.Select(ff => ff.FaceId).ToArray());

Identificamos las caras dentro de la gente del grupo.

 
var person = await faceServiceClient.GetPersonAsync(GroupGUID, result.Candidates[0].PersonId);

Obtenemos a la persona por Guid.

Como se puede ver, la API nos provee de una manera de funcionar bastante lógica.
En primer lugar, tenemos que crear un grupo de personas, después añadir personas a ese grupo, añadir caras a esas personas, y después entrenar el grupo. Esto solo es necesario hacerlo una vez para entrenar el grupo, con eso, podemos pasar a identificar a personas.

Para identificar, los pasos son también sencillos, en primer lugar, detectar las caras en la imagen, en segundo lugar, detectar a quien puede pertenecer esa cara, y en último lugar (en caso de no haberlo almacenado locamente), obtener a la persona a quien pertenece esa cara.

Código completo:

 
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Configuration;
using System.Windows.Forms;
using Microsoft.ProjectOxford.Face;
using System.IO;

namespace PostAzureFaceAPI
{
    public partial class MainForm : Form
    {
        string FaceAPIKey = ConfigurationManager.AppSettings["FaceAPIKey"];
        string FaceAPIEndPoint = ConfigurationManager.AppSettings["FaceAPIEndPoint"];
        string GroupGUID = Guid.NewGuid().ToString();

        public MainForm()
        {
            InitializeComponent();
        }

        private async void btn_Train_Click(object sender, EventArgs e)
        {
            //Abrimos un dialogo de seleccion de carpetas
            FolderBrowserDialog dialog = new FolderBrowserDialog();
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                //Si se ha seleccionado un directorio, hacemos su info
                DirectoryInfo directory = new DirectoryInfo(dialog.SelectedPath);

                //Comprobamos que el directorio tiene carpetas de personas
                if (directory.GetDirectories().Count() == 0)
                    return;

                //=====Empezamos a crear el grupo de trabajo
                //Creamos el cliente
                var faceServiceClient = new FaceServiceClient(FaceAPIKey, FaceAPIEndPoint);

                //Vamos a trabajar desde 0 siempre, asi que comprobamos si hay grupos, y si los hay los borramos
                var groups = await faceServiceClient.ListPersonGroupsAsync();
                foreach (var group in groups)
                {
                    await faceServiceClient.DeletePersonGroupAsync(group.PersonGroupId);
                }
                //Creamos un grupo
                await faceServiceClient.CreatePersonGroupAsync(GroupGUID, "FixedBuffer");

                foreach (var person in directory.GetDirectories())
                {
                    //Comprobamos que tenga imagenes
                    if (person.GetFiles().Count() == 0)
                        return;

                    //Obtenemos el nombre que le vamos a dar a la persona
                    var personName = person.Name;

                    lbl_Status.Text = $"Entrenando a {personName}";

                    //Añadimos a una persona al grupo
                    var personResult = await faceServiceClient.CreatePersonAsync(GroupGUID, personName);

                    //Añadimos todas las fotos a la persona
                    foreach (var file in person.GetFiles())
                    {
                        using (var fStream = File.OpenRead(file.FullName))
                        {
                            try
                            {
                                //Cargamos la imagen en el pictureBox
                                pct_Imagen.Image = new Bitmap(fStream);
                                //Reiniciamos el Stream
                                fStream.Seek(0, SeekOrigin.Begin);
                                // Actualizamos las caras en el servidor
                                var persistFace = await faceServiceClient.AddPersonFaceInPersonGroupAsync(GroupGUID, personResult.PersonId, fStream, file.FullName);
                            }
                            catch (FaceAPIException ex)
                            {
                                lbl_Status.Text = "";
                                MessageBox.Show($"Imposible seguir, razón:{ex.ErrorMessage}");
                                return;
                            }
                        }
                    }
                }

                try
                {
                    //Entrenamos el grupo con todas las personas que hemos metido
                    await faceServiceClient.TrainPersonGroupAsync(GroupGUID);

                    // Esperamos a que el entrenamiento acabe
                    while (true)
                    {
                        await Task.Delay(1000);
                        var status = await faceServiceClient.GetPersonGroupTrainingStatusAsync(GroupGUID);
                        if (status.Status != Microsoft.ProjectOxford.Face.Contract.Status.Running)
                        {
                            break;
                        }
                    }

                    //Si hemos llegado hasta aqui, el entrenamiento se ha completado
                    btn_Find.Enabled = true;
                    lbl_Status.Text = $"Entrenamiento completado";
                }
                catch (FaceAPIException ex)
                {
                    lbl_Status.Text = "";
                    MessageBox.Show($"Response: {ex.ErrorCode}. {ex.ErrorMessage}");
                }
                GC.Collect();
            }
        }

        private async void btn_Find_Click(object sender, EventArgs e)
        {
            lbl_Status.Text = "";
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.DefaultExt = ".jpg";
            dialog.Filter = "Image files(*.jpg, *.png, *.bmp, *.gif) | *.jpg; *.png; *.bmp; *.gif";
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                var imagePath = dialog.FileName;

                //Creamos el cliente
                var faceServiceClient = new FaceServiceClient(FaceAPIKey, FaceAPIEndPoint);

                using (var fStream = File.OpenRead(imagePath))
                {
                    //Cargamos la imagen en el pictureBox
                    pct_Imagen.Image = new Bitmap(fStream);
                    //Reiniciamos el Stream
                    fStream.Seek(0, SeekOrigin.Begin);

                    try
                    {
                        //Detectamos las caras
                        var faces = await faceServiceClient.DetectAsync(fStream);

                        //Detectamos a las personas
                        var results = await faceServiceClient.IdentifyAsync(GroupGUID, faces.Select(ff => ff.FaceId).ToArray());

                        //Creamos una lista de caras y nombres asociados
                        List<(Guid, string)> detections = new List<(Guid, string)>();
                        foreach (var result in results)
                        {
                            //En caso de no haber encontrado un candidato, nos lo saltamos
                            if (result.Candidates.Length == 0)
                                continue;
                            var faceId = faces.FirstOrDefault(f => f.FaceId == result.FaceId).FaceId;
                            //Consultamos los datos de la persona detectada
                            var person = await faceServiceClient.GetPersonAsync(GroupGUID, result.Candidates[0].PersonId);

                            //Añadimos a la lista la relacion
                            detections.Add((faceId,person.Name));            
                        }

                        var faceBitmap = new Bitmap(pct_Imagen.Image);

                        using (var g = Graphics.FromImage(faceBitmap))
                        {                           

                            var br = new SolidBrush(Color.FromArgb(200, Color.LightGreen));

                            // Por cada cara reconocida
                            foreach (var face in faces)
                            {
                                var fr = face.FaceRectangle;
                                var fa = face.FaceAttributes;

                                var faceRect = new Rectangle(fr.Left, fr.Top, fr.Width, fr.Height);
                                Pen p = new Pen(br);
                                p.Width = 50;
                                g.DrawRectangle(p, faceRect);                                

                                // Calculamos la posicon del rectangulo
                                int rectTop = fr.Top + fr.Height + 10;
                                if (rectTop + 45 > faceBitmap.Height) rectTop = fr.Top - 30;

                                // Calculamos las dimensiones del rectangulo                     
                                g.FillRectangle(br, fr.Left - 10, rectTop, fr.Width < 120 ? 120 : fr.Width + 20, 125);

                                //Buscamos en la lista de relaciones cara persona
                                var person = detections.Where(x => x.Item1 == face.FaceId).FirstOrDefault();
                                var personName = person.Item2;
                                Font font = new Font(Font.FontFamily,90);
                                //Pintamos el nombre en la imagen
                                g.DrawString($"{personName}",
                                             font, Brushes.Black,
                                             fr.Left - 8,
                                             rectTop + 4);
                            }
                        }
                        pct_Imagen.Image = faceBitmap;
                    }
                    catch (FaceAPIException ex)
                    {

                    }
                }
            }
        }
    }
}

Para poder utilizar el código, debemos modificar el App.config cambiando las claves:

 
  <appSettings> 
    <add key="FaceAPIKey" value=""/> 
    <add key="FaceAPIEndPoint" value=""/> 
  </appSettings> 

Por las que hemos obtenido al crear el servicio. Como se puede ver en el funcionamiento, la carga se hace desde seleccionando una carpeta, que a su vez tenga dentro una carpeta por cada persona que queramos entrenar y que se llame como la persona, será dentro de cada una de estas carpetas donde metamos las fotos de cada persona:

train

Una vez que hayamos completado el entrenamiento, podremos seleccionar una imagen en la que identificar a la persona:

resultado

Como hemos podido ver, es muy sencillo empezar a utilizar la API de reconocimiento e identificación facial. En próximas entradas, seguiremos profundizando en los Cognitive Services de Azure, tanto conociendo el resto de los servicios, como profundizando en este, que ofrece muchas más opciones aparte de la identificación de personas.

**La entrada Reconociendo a personas mediante Azure FaceAPI se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Koalite

Mis tecnologías de 2018

enero 08, 2019 05:06

Entre los post tradicionales de este blog se encuentra el repaso de las tecnologías que he usado profesionalmente a lo largo del año. Si alguien tiene curiosidad por ver la evolución, puede revisar lo que usé en el 2015, 2016 y 2017.

Como el contexto es importante, vamos a centrar un poco desde qué perspectiva está escrito este post.

Sólo voy a mencionar tecnologías que uso profesionalmente, no aquellas con la que haya podido jugar un rato pero no haya tenido en producción. Para eso ya están otros posts de este blog.

También es fundamental recordar que me dedico a productos, no proyectos. Además son productos (software para puntos de venta) que se despliegan localmente en las máquinas y en los cuales no controlamos el proceso de despliegue, el cual se realiza de forma manual no por una cuestión técnica sino de negocio: nuestros clientes son distribuidores que se ganan la vida vendiendo nuestro producto y cobrando por cosas como ésta. Si a esto le unes la importancia de mantener la compatibilidad con versiones anteriores del software, y sistemas operativos entre antiguos y obsoletos, el margen de maniobra que nos queda para jugar con nuevas tecnologías es menor y preferimos garantizar la estabilidad de todo el sistema.

.NET, servidores y escritorio

La base de nuestras aplicaciones está desarrollada usando .NET Framework 3.5 SP1. Sí, es antiguo, pero tenemos mucho parque instalado cuya actualización no es trivial (o ni siquiera está soportada). También tenemos algunos desarrollos de servidor “puro” en los que controlamos el despliegue y usamos versiones más modernas de .NET Framework (tradicional, no .NET Core), pero representa una parte mucho menor de nuestro código.

Aquí miro con envidia algunas funcionalidades nuevas que ofrecen las últimas versiones de C#, pero para usarlas tendríamos que actualizar el tooling (seguimos con Visual Studio 2013 y el msbuild y Resharper de aquella época) y, hasta ahora, no nos ha compensado.

Mantenemos nuestro stack clásico basado en NHibernate, Castle.Windsor, log4net y NUnit. Son piezas que nos funcionan muy bien, las conocemos perfectamente para explotar sus ventajas y minimizar sus inconvenientes, y la migración a otras librerías no creemos que nos aporte lo suficiente como para compensar el coste de hacerlo.

Como servidor de base de datos seguimos usando Microsoft SQL Server, fundamentalmente las versiones 2005 y 2014. La realidad es que tampoco usamos nada especial del 2014 (más allá de que en la versión Express es algo más generoso en tamaño de base de datos) y nos ceñimos a lo soportado por 2005 para mantener compatibilidad. Tampoco es que tengamos unos requisitos muy elevados en este aspecto.

Clientes web

Cada vez más hemos ido migrando las aplicacones escritas en .NET (usando Windows Forms) a tecnologías web.
Ahí se nota más la evolución a lo largo de los años (se puede ver también en los posts que he ido escribiendo sobre tecnologías front) y tenemos variedad de librerías, frameworks y lenguajes. Eso complica el mantenimiento, pero de momento no lo suficiente como para justificar la reescritura de aplicaciones, que están funcionando bien y generando negocio, en aras de usar The Best Platform™.

Tenemos cosas con AngularJS 1.x (estamos deseando quitárnoslo de encima, pero es difícil de justificar económicamente) y con varias versiones de ReactJS (todas relativamente añejas). Tenemos cosas con ES5, otras con ES2015 y otras con TypeScript.

Para hacerlo más entretenido, cada una usa el sistema de compilación que mejor nos pareció en cada momento, incluyendo Grunt, Webpack o simples scripts npm.

En la parte de testing en frontend usamos estrategias variadas, pero al final utilizamos las librerías típicas: jasmine, mocha, chai, protractor y karma (para la parte de angular), nightwatch para tests de extremo a extremo con React. Con Enzyme ya el año anterior vimos que acabábamos teniendo tests que nos resultaban poco útiles y creo que este año no hemos escrito ningún test nuevo con él (incluso hemos borrado algunos existentes para no tener que mantenerlos).

La nube

Este año hemos empezado a prestar más atención a la nube. Nos hemos encontrado con la necesidad de ofrecer determinados servicios mantenidos por nosotros y eso nos ha llevado a aprovechar la parte de IaaS para levantar servidores dedicados para algunos clientes.

De momento estamos usando como proveedor Azure (paso lógico dado nuestro bagaje), y por el camino hemos comenzado también a utilizar algunos servicios concretos de Azure, como Azure Blobs. Imagino que poco a poco iremos usando más servicios de Azure y atándonos más a él. No es algo que me deje demasiado tranquilo, pero bueno.

En nuestra (limitadísima) experiencia con la nube y, especialmente, con Azure, ha habido cosas que nos han gustado más y cosas que nos han gustado menos, como era de esperar.

Por una parte, la flexibilidad para levantar máquinas, aumentar o disminuir su potencia en función de las necesidades, gestionar backups, infraestructura de red, etc., es muy práctica. Como todas las cosas para pobres, lo barato acaba saliendo caro a la larga y los costes cuando empiezas a mirar a medio plazo pueden ser más elevados que con sistemas propios, pero la comodidad y la disminución de la barrera de entrada compensa en muchos casos.

En la parte mala, a veces nos quedamos con la idea de que avanza todo un poco demasiado rápido. Y eso no es necesariamente malo, sino fuese porque te quedas con la impresión de producto a medio hacer. Que estén cambiando continuamente el UI del portal, que tengas unas cuantas cosas en preview, otras marcadas como classic (que suena a que las vas a descontinuar pasado mañana), otras con v1, v2, v3…

Genera cierta sensación de inseguridad y no saber realmente qué puedes usar y hasta cuándo. Nos ocurrió por ejemplo con la parte de RBAC que nos gustó mucho pero cuando fuimos a implementarla nos encontramos con algunos problemas por no estar completamente integrada con otros servicios.

(D)VCS e integración continua

En esta parte es donde más cambios hemos introducido este año. Después de diez años usando Subversion para gestionar el código y CruiseControl.NET para lanzar nuestros scripts de compilación, hemos dado el salto a GitLab.

No es que hubiera dejado de funcionar lo que teníamos, pero empezaba a suponer demasiada fricción para algunas cosas que queríamos hacer.

El rendimiento de SVN ya se hacía difícil de soportar con el volumen de código que teníamos. Por su parte, CCNET nos complicaba la parte de compilar varias ramas de una misma aplicación. Lo hacíamos, pero de forma muy manual. Eso, unido a una suite de tests grande que tarda mucho en ejecutarse hacía que la rama principal se rompiese con frecuencia porque para evitar lanzar los tests en nuestros equipos de desarrollo subíamos directamente y dejábamos que se pasaran arriba.

Con GitLab conseguimos un sistema de control de versiones más rápido y potente, que nos deja hacer más cosas, y de paso la gestión de pipelines de compilación.

La migración de la parte de código no ha existido (adiós a la historia, que sigue viviendo en SVN) y ha consistido más en empezar a usar git “en serio”. Todos conocíamos git, pero no lo habíamos explotado realmente. De momento está siendo una buena experiencia.

Adaptar nuestros scripts de compilación ha sido sencillo porque siempre hemos apostado por independizarlos de la herramienta que los lanza. Eso nos ha permitido aprovechar para automatizar un par de procesos manuales que nos quedaban y montar algo parecido a un sistema de entrega continua hacia la gente de QA que nos ayuda a detectar bugs antes.

Después de probar unas cuantas (bastantes) herramientas para usar git, todos hemos acabado adoptando Git Bash como interfaz principal, apoyándonos en alguna otra cosa para tareas puntuales. Aquí me quedo con la espinita clavada de magit, que me encantó por filosofía y usabilidad, pero que es inmanejable con repositorios grandes en Windows.

Resumen

Viendo la evolución tecnológica que hemos tenido en los últimos años está claro que no formamos un equipo que esté trabajando siempre con lo último de lo último. Como decía al principio, el contexto en que nos movemos hace que eso no sea práctico y necesitemos ser más conservadores.

Sin embargo, que tengamos que ser conservadores al adoptar nuevas tecnologías no nos exime de tener que estar pendientes de lo que va surgiendo y conocer qué nos ofrece esa nueva moda superinteresante de la que todo habla (spoiler: muchas veces, más de lo mismo). Puede que no lo pongamos en producción en la versión 1, y ni siquiera en la versión 2, pero si realmente nos soluciona un problema, es probable que acabemos usándolo.

En lo que sí tenemos mucho cuidado es en evitar la tecnología por la tecnología. Jugar con cosas nuevas y cambiar de plataformas cada cierto tiempo es muy divertido y tentador, pero desde un punto de vista puramente económico (y no olvidemos que aquí estamos hablando de tecnologías que usamos profesionalmente), casi nunca es rentable.

Posts relacionados:

  1. Resumen 2018
  2. Mis tecnologías del 2017
  3. Mis tecnologías del 2016

» Leer más, comentarios, etc...

Meta-Info

¿Que es?

Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

rss subscripción

Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
planetacodigo

planetacodigo

Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

Idea: Juanjo Navarro

Diseño: Albin