Weblogs Código

Variable not found

Enlaces interesantes 350

febrero 18, 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

ASP.NET Core developer roadmap

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Machine learning

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

Publicado en: www.variablenotfound.com.

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

Koalite

Clases estáticas como alternativa a inyección de dependencias

febrero 18, 2019 05:06

En las aplicaciones orientadas a objetos es frecuente que en algún momento necesites tener varias implementaciones de un mismo contrato. Esto no es ningún problema y puedes aprovechar el uso de interfaces o clases abstractas para definir el contrato y luego crear distintas implementaciones del mismo.

Al tener varias implementaciones, ahora toca buscar una forma de decidir qué implementación hay que utilizar en cada caso. Para esto hay varias fórmulas. Desde el clásico patrón factoría para delegar la creación de la implementación concreta a otra clase, hasta el uso de contenedores de inversión de control e inyección de dependencias. Hoy en día parece que las buenas prácticas te llevan a usar estos últimos, pero quizá andar inyectando dependencias no siempre sea la mejor opción.

Siguiendo con las “buenas prácticas”, otra de las cosas que parece que deberíamos hacer es evitar a toda costa los métodos estáticos porque son menos flexibles y limitan el polimorfismo. Sin embargo, hay escenarios en los que un uso razonable de clases estáticas pueden ayudarnos a conseguir esa parte de comportamiento dinámico que necesitamos sin necesidad de introducir grandes cambios en el diseño de la aplicación, algo que es especialmente útil cuando estamos tratando con código legacy.

En este post vamos a partir de un ejemplo (basado en hechos reales) para ver cómo poco a poco podemos modificar una aplicación que parte de un diseño completamente rígido para dotarla de cierta flexibilidad sin asumir demasiados riesgos y sin añadir excesiva complejidad al diseño.

El problema

Partimos de una aplicación que realiza un proceso (que nos da más o menos igual), durante el cual se van ejecutando acciones modeladas en distintas clases. Esta aplicación es ejecutada manualmente por un usuario que va viendo en pantalla el estado de las operaciones y al que se le muestran de vez en cuando algunos mensajes.

El código podría ser de este estilo:

public class UserConfigurator {
  public void AddUser() {
    ProgressView.ShowWhile("Adding users...", () => {
      // ... 
    });

    MessageBox.ShowInfo("Users added");
  }
}

public class FileDeployer {
  public void DeployFiles() {
    if (!CanDeployFiles()) {
      MessageBox.ShowError("Could not deploy files");
      var helpView = new HelpView();
      helpView.LoadDocument("deploy-files.html");
      helpView.Show();
    }
    // ...
  }
}

Como éstas, habría unas cuantas clases más cada una encargada de realizar parte del proceso, comunicar al usuario el estado. Supongamos ahora que necesitamos modificar esta aplicación para que se pueda ejecutar en modo silencioso, sin mostrar ningún tipo de mensaje al usuario, para ser ejecutada de forma desatendida.

Siempre podríamos ir revisando el código con cariño e ir añadiendo ifs para sacar o no sacar ventanas, pero no parece una cosa muy agradable de hacer (ni de mantener).

Existen muchas alternativas, pero en general partimos de una situación relativamente incómoda, porque tenemos bastante código dedicado a mostrar cosas en pantalla, repartido por bastantes sitios de la aplicación, y el diseño original no usa inyección de dependencias o cualquiera de las técnicas típicas para resolver esto.

Ocultando el polvo

Para intentar mejorar la situación, lo primero que podemos hacer es intentar encapsular todo el subsistema de interfaz de usuario detrás de una fachada. En lugar de tener acciones a través del método ProcessView.ShowWhile, MessageBox.ShowInfo y MessageBox.ShowError, podemos construir algo así:

public static class UI {
  public static void ShowWhile(string message, Action action) {
    ProgressView.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    MessageBox.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    MessageBox.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
     var view = new HelpView();
     view.LoadDocument(file);
     view.Show(); 
  }
}

En la fachada encapsulamos todas las operaciones relativas a interfaz de usuario. Además, las vamos a encapsular como métodos estáticos. Esto nos permite evitar tener que gestionar el ciclo de vida de la fachada. Si fueran métodos de instancia y la fachada tuviera estado (algo que vamos a necesitar en un rato), tendríamos que andar garantizando que el estado de la fachada es el adecuado en todas las instancias y la cosa se complica. Es uno de esos casos en los que si puedes evitar clases y limitarte a módulos (que es lo que realmente es la clase estática), te ahorrarás complicaciones.

Con la fachada montada así, actualizar el código del resto de la aplicación es trivial:

public class UserConfigurator {
  public void AddUser() {
    UI.ShowWhile("Adding users...", () => {
      // ... 
    });

    UI.ShowInfo("Users added");
  }
}

public class FileDeployer {
  public void DeployFiles() {
    if (!CanDeployFiles()) {
      UI.ShowError("Could not deploy files");
      UI.ShowHelpDocument("deploy-files.html");
    }
    // ...
  }
}

Por el camino, al haber pasado algo de lógica a la fachada (como el caso del ShowHelpDocument), simplificamos algo el código de los clientes y gracias a la fachada tenemos encapsulado todo el código relativo a la comunicación con el usuario sacando ventanas.

Introduciendo polimorfismo

Ahora sólo necesitamos tener dos implementaciones de fachada, una que muestre mensajes al usuario y otra que se limite a escribir esos mensajes en un log en lugar de andar sacando ventanas. Para ello, vamos a empaquetar toda la lógica actual de la fachada en una clase y haremos que la fachada delegue en ella todas las operaciones:

public static class UI {

  private class WindowsUI {
    public void ShowWhile(string message, Action action) {
      ProgressView.ShowWhile(message, action);
    }
    public void ShowError(string message) {
      MessageBox.ShowError(messge);
    }
    public void ShowInfo(string message) {
      MessageBox.ShowInfo(messge);
    }
    public ShowHelpDocument(string file) {
       var view = new HelpView();
       view.LoadDocument(file);
       view.Show(); 
    }
  }

  private static readonly _ui = new WindowsUI();

  public static void ShowWhile(string message, Action action) {
    _ui.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    _ui.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    _ui.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
     _ui.ShowHelpDocument(file);
  }
}

Nuevamente, una refactorización trivial. He dejado la clase WindowsUI como privada a la fachada porque realmente no tiene sentido fuera de ella, pero si ten ponen nervioso las clases grandes, te lo puedes llevar fuera.

Ahora ya está bastante claro, sólo nos falta extraer un interface con las operaciones de WindowsUI para poder crear nuestra implementación silenciosa:

public static class UI {

  private interface IUI {
    void ShowWhile(string message, Action action);
    void ShowError(string message);
    void ShowInfo(string message);
    void ShowHelpDocument(string file);
  }

  private class WindowsUI: IUI {
    // Implementación que hemos visto antes
  }

  private class SilentUI : IUI {
    public void ShowWhile(string message, Action action) {
      Log.Info(message);
      action();
    }
    public void ShowError(string message) {
      Log.Error(message);
    }
    public void ShowInfo(string message) {
      Log.Info(message);
    }
    public void ShowHelpDocument(string file) {
      // Nada que hacer aquí
    }
  }

  private static readonly _ui = new WindowsUI();

  public static void EnableSilentMode() {
    _ui = new SilentUI();
  }

  public static void ShowWhile(string message, Action action) {
    _ui.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    _ui.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    _ui.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
    _ui.ShowHelpDocument(file);
  }
}

Sólo nos falta activar el modo silencioso al arrancar la aplicación en base a algún parámetro de línea de comandos:

static void Main(string[] args) {
  if (args.Contains("--silent")) {
    UI.EnableSilentMode();
  }
  // ...
}

Con esto ya podemos configurar de forma transparente a toda la aplicación si queremos que nuestro interfaz de usuario muestre ventanas o sea completamente silencioso y se limite a escribir en un fichero.

Resumen

Tendemos a guiarnos demasiado por las “buenas prácticas” y eso nos lleva a utilizar siempre las mismas soluciones para determinados problemas, aunque podamos encontrar soluciones alternativas más simples.

El ejemplo que hemos visto en este post es sólo eso, un ejemplo, y no pretendo en ningún caso decir que reniegues de tus contenedores de inyección de dependencias para usar sólo fachadas estáticas, entre otras cosas porque esta aproximación también tiene sus pegas (como la dificultad para seguir las dependencias de cada clase).

Sin embargo, cuando te encuentras con una aplicación que no está usando un contenedor, es bueno conocer alternativas que te permiten modificar la aplicación e introducir cierto comportamiento polimórfico sin tener que reescribir media aplicación para usar inyección de dependencias.

Posts relacionados:

  1. Las ventajas de NO usar inyección de dependencias
  2. Inyección de dependencias en Javascript (y otros lenguajes dinámicos)
  3. Fachadas estáticas para tratar con estado global

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

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...

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...

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