Quantcast
Channel: Planeta Código
Viewing all 2713 articles
Browse latest View live

Variable not found: Enlaces interesantes 345

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

Poesía Binaria: Cómo utilizar PHP desde contenedores docker tanto de forma local como en producción

$
0
0

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-yless \
    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)-eq0]; 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
readonlySCRIPTPATH="$(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
readonlySCRIPTPATH="$(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="
$(getentpasswd1000|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_DIR2>/dev/null; exec php ${QUOTED_ARGS}"
else
    docker exec -i myphp7.2-fpm bash -c "
cd$TRANSLATED_DIR2>/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.

proyectos Ágiles: Cómo desescalar una organización

$
0
0

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 articular 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,).

Artículos relacionados:

Presentaciones relacionadas

Artículos relacionados:

Variable not found: ¿Se pueden introducir directivas o lógica de inicialización en todas las vistas y páginas sin duplicar código?

$
0
0
ASP.NET CoreComo sabemos, los archivos Razor _ViewImports.cshtml y _ViewStart.cshtml se emplean para introducir directivas y código de inicialización para todas las vistas o páginas que se encuentren por debajo en la estructura de carpetas del proyecto.

Es decir, si el archivo _ViewImports.cshtml se encuentra en la carpeta /Views, todas las vistas que hay por debajo heredarán las directivas que hayamos especificado en su interior, de la misma forma que /Pages/_ViewImports.cshtml afectara a las páginas presentes en la carpeta /Pages y descendientes.

Pues bien, hace unos días, un alumno del curso de ASP.NET Core en CampusMVP (que, por cierto, ha sido recientemente actualizado a la versión 2.2 del framework) planteaba una duda sobre un escenario algo más complejo. Si una aplicación tenía vistas en la carpeta /Views, también tenía vistas en áreas (carpeta /Areas), e incluso pudiera tener algunas páginas Razor en /Pages, la pregunta era si existía algún mecanismo para hacer que todas las vistas o páginas definidas en dichas carpetas compartieran directivas (como using o importaciones de tag helpers) sin tener que duplicar este código en sus respectivos _ViewImports.cshtml para cada una de ellas.

A priori pensé que quizás el planteamiento sería retocar ligeramente el motor de vistas, pero, tras estudiarlo un rato, vi que el tema era mucho más sencillo :)

Y es que, aunque no es algo muy intuitivo, resulta que los archivos _ViewImports.cshtml y _ViewStart.cshtml son procesados comenzando por el directorio raíz del proyecto, y no desde carpetas específicas para vistas, como podría suponerse.  Es decir, si se fuera a compilar una vista en /Areas/Dashboard/Views/Home/Index.cshtml, el framework cuando se va a compilar una página o vista, el framework incluirá en ella, sucesivamente, las directivas que encuentre en:
  • /_ViewImports.cshtml (la carpeta raíz del proyecto)
  • /Areas/_ViewImports.cshtml
  • /Areas/Dashboard/_ViewImports.cshtml
  • /Areas/Dashboard/Views/_ViewImports.cshtml
  • /Areas/Dashboard/Views/Home/_ViewImports.cshtml
Y lo mismo ocurre con el archivo _ViewStart.cshtml.

Por tanto, para introducir directivas o código de inicialización Razor de forma global para toda la aplicación, bastará con establecerlas en el raíz del proyecto.

En fin, son esos pequeños detalles que pueden venir bien en algunas ocasiones y es interesante conocer :)

Publicado en Variable not found.

Blog Bitix: Generar, procesar y modificar documentos JSON con JSON-P en Java

$
0
0
Java

Los servicios que ofrecen una API REST normalmente emplean JSON como formato para intercambiar datos tanto en las peticiones como en las respuestas. En Java hay varias formas de generar y procesar JSON para obtener los datos que contiene.

Una de las formas estándar es usando la especificación JSON-P que convierte un JSON a una estructura de objetos Java que representan los datos del JSON como son los objetos Json, JsonObject, JsonArray, JsonString o JsonNumber. Esta API permite convertir una cadena de texto en formato JSON a objetos de la API y una jerarquía de objetos de la API a una cadena.

 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
packageio.github.picodotdev.blogbitix.javajson;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{Compradorcomprador=buildComprador();Stringjson="";// JSON-P
JsonObjectjsonp=Json.createObjectBuilder().add("name",comprador.getNombre()).add("edad",comprador.getEdad()).add("direcciones",Json.createArrayBuilder().add(Json.createObjectBuilder().add("calle",comprador.getDirecciones().get(0).getCalle()).add("ciudad",comprador.getDirecciones().get(0).getCiudad()).add("codigoPostal",comprador.getDirecciones().get(0).getCodigoPostal()).add("pais",comprador.getDirecciones().get(0).getPais()).build()).add(Json.createObjectBuilder().add("calle",comprador.getDirecciones().get(1).getCalle()).add("ciudad",comprador.getDirecciones().get(1).getCiudad()).add("codigoPostal",comprador.getDirecciones().get(1).getCodigoPostal()).add("pais",comprador.getDirecciones().get(1).getPais())).build()).build();json=jsonp.toString();jsonp=Json.createReader(newStringReader(json)).readObject();System.out.printf("JSON-P: %s%n",json);System.out.printf("JSON-P (JsonObject): %s%n",jsonp.toString());...}privatestaticCompradorbuildComprador(){Compradorcomprador=newComprador();comprador.setNombre("Juan");comprador.setEdad(30);comprador.getDirecciones().add(buildDireccion());comprador.getDirecciones().add(buildDireccion());returncomprador;}privatestaticDireccionbuildDireccion(){returnnewDireccion("calle","ciudad","codigoPostal","pais");}}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ./gradlew run
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :run
JSON-P: {"name":"Juan","edad":30,"direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"},{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}]}
JSON-P (JsonObject): {"name":"Juan","edad":30,"direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"},{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}]}
...
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

JSON-P permite analizar un JSON recibiendo un flujo de eventos en su lectura con la Stream API además de soportar las especificaciones JSON Pointer para construir una referencia a una valor del documento JSON que junto con JSON Patch y JSON Merge Patch permite realizar operaciones de modificación a un documento JSON o un recurso en una API REST con el verbo HTTP PATCH. Las clases en la API son JsonPointer, JsonPatch y JsonMergePatch.

En el ejemplo se agrega un nuevo campo y se elimina un elemento de un array a un documento JSON haciendo uso de JSON Pointer y JSON Patch. En los documentos de las especificaciones se incluyen algunos ejemplos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
packageio.github.picodotdev.blogbitix.javajson;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{...JsonPatchjsonPatch=Json.createPatchBuilder().add("/telefono","111111111").remove("/direcciones/0").build();jsonp=jsonPatch.apply(jsonp);System.out.printf("JSON-P (JsonPatch): %s%n",jsonPatch.toString());System.out.printf("JSON-P (JsonObject): %s%n",jsonp.toString());...}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ ./gradlew run
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :run
...
JSON-P (JsonPatch): [{"op":"add","path":"/telefono","value":"111111111"},{"op":"remove","path":"/direcciones/0"}]
JSON-P (JsonObject): {"name":"Juan","edad":30,"direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}],"telefono":"111111111"}
...
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

JSON-P es una API de bajo nivel para procesar JSON y un tanto engorrosa de utilizar, no es la única forma utilizable. También está la especificación JSON-B que va un poco más lejos que JSON-P y ofrece una forma de hacer una correspondencia entre el JSON y tipos propios en Java que siguen las convenciones de los Java Bean, con esto se aprovecha la validación de tipos de Java. Las librerías Gson y Jackson también ofrece soporte para tratar con JSON de forma parecida a JSON-B. Otra alternativa es utilizar JsonPath que permite extraer datos de una cadena JSON con expresiones de selección.

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.

Koalite: TypeScript: varianza y solidez

$
0
0

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

Variable not found: Enlaces interesantes 346

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

Poesía Binaria: Notifica, logea y enriquece tu experiencia de trabajo en Bash con este script

$
0
0

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

readonlyBASHISTANT_DIR=$HOME/.bashistant
readonlyBASHISTANT_LOCK=$BASHISTANT_DIR/lock
readonlyBASHISTANT_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=

readonlyMYPID=$$

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"";
        readonlyWINDOWPID=$(ps-o ppid,pid|grep$$|awk'{print $1;exit}')
        ifxset q &>/dev/null &&hash xdotool; then
                readonlyWINDOWID=$(xdotool search --pid$WINDOWPID|tail -1)
        fi
        COMMANDS=()
}

__bashistant_init

function __bashistant_get_timestamp(){
        date +%s
}

function __bashistant_print_info(){
        localINFO="${BASHISTANT_INFO_PADDING}$1"

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


Blog Bitix: Reproducir audio y música MIDI y sampled (wav, mp3, ogg) con Java y FFmpeg

$
0
0
Java
FFmpeg

En la API de Java en el paquete javax.sound.sampled hay unas pocas clases que permiten reproducir archivos de música o sonidos y en el paquete java.sound.midi contiene clases para la música o sonidos digitales o sintetizados. Los tipos de archivos de música o sonidos soportados son wav, au, aif para los archivos sampled, y archivos midi para los digitales. Nativamente Java con las clases incluidas en el JDK no puede reproducir varios formatos de archivo de sonido populares como mp3 u ogg.

Como Java no soporta muchos tipos de archivos para reproducir los no soportados hay que hacer una conversión a alguno de los si soportados, por ejemplo de mp3 a wav. FFmpeg es un programa con el que se pueden hacer conversiones de archivos de sonido y recodificaciones de archivos de vídeo que junto con la posibilidad de invocar desde Java un proceso del sistema habilita reproducir archivos mp3 u ogg desde Java.

La clase principal de la API de sonido es AudioSystem para los archivos sampled y MidiSystem para los archivos midi, con los métodos getAudioFileTypes() y getMidiFileTypes() se obtienen los archivos de audio soportados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
packageio.github.picodotdev.blogbitix.javasound;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{printSupportedFileTypes();...}privatestaticvoidprintSupportedFileTypes(){StringaudioFileTypes=Arrays.stream(AudioSystem.getAudioFileTypes()).map(it->it.getExtension()).collect(Collectors.joining(", "));StringmidiFileTypes=Arrays.stream(MidiSystem.getMidiFileTypes()).mapToObj(it->String.valueOf(it)).collect(Collectors.joining(", "));System.out.printf("Audio file types: %s%n",audioFileTypes);System.out.printf("Midi file types: %s%n",midiFileTypes);System.out.printf("Mixers info: %s%n",Arrays.stream(AudioSystem.getMixerInfo()).map(it->it.toString()).collect(Collectors.joining(", ")));}...}
1
2
3
Audio file types: wav, au, aif
Midi file types: 0, 1
Mixers info: Port PCH [hw:0], version 4.20.3-arch1-1-ARCH, default [default], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,0], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,3], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,7], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,8], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,9], version 4.20.3-arch1-1-ARCH, PCH [plughw:0,10], version 4.20.3-arch1-1-ARCH

Para reproducir un archivo midi hay que usar las clases MidiSystem, Sequence y Sequencer.

 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
packageio.github.picodotdev.blogbitix.javasound;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{printSupportedFileTypes();Stringformat=args[0];switch(format){case"midi":playMidi();break;case"wav":playWav();break;case"mp3":playMp3();break;case"ogg":playOgg();break;}}...privatestaticvoidplayMidi()throwsException{InputStreamis=Main.class.getResourceAsStream("/midi.mid");Sequencersequencer=MidiSystem.getSequencer();sequencer.open();sequencer.setSequence(is);System.out.printf("Midi duration: %d seconds%n",sequencer.getMicrosecondLength()/1000000);sequencer.start();do{Thread.sleep(100);}while(sequencer.isRunning());sequencer.close();is.close();}...}
1
2
Midi duration: 216 seconds

Para reproducir un archivo wav hay que usar las clases AudioSystem, AudioInputStream y Clip.

 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
packageio.github.picodotdev.blogbitix.javasound;...publicclassMain{...privatestaticvoidplayWav()throwsException{InputStreamis=Main.class.getResourceAsStream("/wav.wav");AudioInputStreamais=AudioSystem.getAudioInputStream(is);Clipclip=AudioSystem.getClip();clip.open(ais);System.out.printf("Audio format: %s%n",ais.getFormat());System.out.printf("Sampled duration: %d seconds%n",clip.getMicrosecondLength()/1000000);clip.start();do{Thread.sleep(100);}while(clip.isRunning());clip.close();ais.close();}...}
1
2
Audio format: PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
Sampled duration: 240 seconds

Para reproducir un archivo mp3 o ogg hay que convertirlo al formato wav, con el comando FFmpeg y usando una tubería leyendo de su salida resultado de la conversión con un InputStream. En el siguiente ejemplo se realiza una conversión de un mp3 a wav con el formato 44100Hz, 2 canales y de 16 bits con un proceso de FFmpeg. Para reproducir un archivo ogg el código es similar.

 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
packageio.github.picodotdev.blogbitix.javasound;...publicclassMain{...privatestaticvoidplayMp3()throwsException{ProcessBuilderbuilder=newProcessBuilder().command("ffmpeg","-i","src/main/resources/mp3.mp3","-acodec","pcm_s16le","-ar","44100","-ac","2","-f","wav","pipe:1");Processprocess=builder.start();InputStreamis=process.getInputStream();AudioInputStreamais=AudioSystem.getAudioInputStream(is);AudioFormataf=ais.getFormat();byte[]bytes=is.readAllBytes();Clipclip=AudioSystem.getClip();clip.open(af,bytes,0,bytes.length);System.out.printf("Audio format: %s%n",ais.getFormat());System.out.printf("Sampled duration: %d seconds%n",clip.getMicrosecondLength()/1000000);clip.start();do{Thread.sleep(100);}while(clip.isRunning());clip.close();ais.close();}...}
1
2
Audio format: PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
Sampled duration: 240 seconds
 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
packageio.github.picodotdev.blogbitix.javasound;...publicclassMain{...privatestaticvoidplayOgg()throwsException{ProcessBuilderbuilder=newProcessBuilder().command("ffmpeg","-i","src/main/resources/ogg.ogg","-acodec","pcm_s16le","-ar","44100","-ac","2","-f","wav","pipe:1");Processprocess=builder.start();InputStreamis=process.getInputStream();AudioInputStreamais=AudioSystem.getAudioInputStream(is);AudioFormataf=ais.getFormat();byte[]bytes=is.readAllBytes();Clipclip=AudioSystem.getClip();clip.open(af,bytes,0,bytes.length);System.out.printf("Audio format: %s%n",ais.getFormat());System.out.printf("Sampled duration: %d seconds%n",clip.getMicrosecondLength()/1000000);clip.start();do{Thread.sleep(100);}while(clip.isRunning());clip.close();ais.close();}}
1
2
Audio format: PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
Sampled duration: 240 seconds

En todos los casos como se muestra en el código es posible también conocer la duración de un archivo de sonido. En realidad al usar FFmpeg cualquier tipo de archivo de sonido que soporte la conversión a wav es reproducible con Java, y no son pocos los soportados incluso muchos no tan populares como el mp3 o ogg.

1
2
$ ffmpeg -codecs
$ ffmpeg -formats

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 --args="midi", ./gradlew run --args="mp3", ./gradlew run --args="ogg".

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

$
0
0

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

Variable not found: Crear y utilizar librerías multiplataforma con C++ y .NET Core (Parte 2)

$
0
0

Blogger invitado

Blogger invitado

Jorge Turrado

Apasionado de la programación, siempre buscando la manera de mejorar el día a día con el desarrollo de tecnologías .NET. Apasionado de este momento tan cambiante y fabuloso para ser desarrollador C#.
Blog: Fixed Buffer
Hola a todos los lectores de Variable Not Found, es un gusto para mi poder hacer esta colaboración con José, en la que vengo a hablaros de cómo poder utilizar código nativo desde .NET Core, sin perder la capacidad de ejecución multiplataforma que nos ofrece este maravilloso framework.

¿Y por qué me debería interesar utilizar código nativo si .NET Core ya es lo bastante rápido? Esa es una muy buena pregunta, es cierto que con .NET Core y su filosofía cloud se ha mejorado muchísimo el rendimiento, y se ha modularizado el framework consiguiendo unos resultados muy buenos.

Sin embargo, imagina que tu aplicación web necesita de un rendimiento excelente, por ejemplo, porque necesites enviarle imágenes y tengas que procesarlas en el servidor para leer un OCR o procesar su contenido. En estos casos, cada ciclo cuenta, ya que afecta directamente a la escalabilidad de tu aplicación; no es lo mismo tardar 10 milisegundos que 20, cuando hablas de un gran número de usuarios concurrentes.

El framework nos da la posibilidad de ejecutar código nativo (ya .NET Framework nos daba esa posibilidad hace mucho), pero el código nativo es código compilado directamente para el sistema operativo, y esto hace que una librería de Windows sólo sirva para Windows, lo mismo que si fuese de Linux o MacOS.

Hoy vamos a abordar ese problema, creando librerías nativas multiplataforma, que podamos compilar sin tener que cambiar nada en ellas (lo cual nos permite tener una única librería que mantener) y consumiéndolas desde .NET Core. Por eso, en mi blog FixedBuffer he dejado la primera parte de esta entrada:

Crear y utilizar librerías multiplataforma con C++ y .NET Core (Parte 1)

A partir de aquí, vamos a considerar que ya tenéis vuestra librería nativa compilada, y vamos a centrarnos solo en la parte de C# y .NET Core.


Llamadas a librerías nativas

.NET CoreLo primero que hay que saber es que las librerías nativas no se referencian dentro del proyecto como haríamos con librerías .NET (administradas), son código no administrado al que tenemos que acceder directamente conociendo los métodos que expone.

Para esto, el framework nos ofrece una herramienta que nos permite consumir código nativo, P/Invoke(dentro del namespaceSystem.Runtime.InteropServices). Gracias a esto, vamos a poder consumir código nativo sin problemas.

Como vimos en la primera parte, cada sistema operativo genera ficheros de librería con diferente extensión, pero esto es capaz de manejarlo .NET Core perfectamente. Supongamos que tenemos algo así:
[DllImport("mylib")]
static extern void DoThing();
Internamente, el framework intentará acceder al binario con los siguientes nombres:
  • mylib.dll
  • mylib.so
  • mylib.dylib
  • libmylib.so
  • libmylib.dylib
El gran problema aquí podría ser si nuestras librerías se llamasen de manera diferente en cada sistema operativo (para nuestro ejemplo no es el caso). Imaginemos que nuestra librería, en vez de ser "mylib", fuese "winlib", "linuxlib" y "maclib"; el framework sería incapaz de resolver esa situación (de hecho, se debate desde 2015), pero podemos echarle una mano evaluando nosotros el sistema operativo a través de la clase RuntimeInformation, que nos permite saber en qué sistema operativo corremos, así como detalles del mismo:
[DllImport("winlib")]
static extern void DoThing_Windows();
[DllImport("linuxlib")]
static extern void DoThing_Linux();
[DllImport("maclib")]
static extern void DoThing_Mac();

public void DoThing()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
DoThing_Windows();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
DoThing_Linux();
else
DoThing_Mac();
}
Esta característica está disponible desde .NET Framework 4.7.1 , en todos los .NET Core y .NET Standard, y no sólo nos permite hacer diferenciación entre llamadas nativas, sino que nos podría servir por ejemplo para el sistema de archivos, ejecución de comandos PS o Bash, o cualquier otra cosa en la que nos interese saber si estamos sobre Windows, Linux o MacOS.

Dicho todo esto, vamos a ponerlo en práctica con la librería que ya hemos preparado, y veamos cómo se comporta, para eso, he creado un ejecutable de consola como el siguiente:
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace PostNetCoreNativo
{
class Program
{
//====================CONSTANTES====================
const int STRING_MAX_LENGTH = 1024;

//====================DECLARACIÓN DE LLAMADAS NATIVAS===================================
[DllImport("EjemploNativo", EntryPoint = "GetStringMessage")]
public static extern void GetStringMessageNativo(StringBuilder sb, int StringLenght);

[DllImport("EjemploNativo", EntryPoint = "Suma")]
public static extern int SumaNativo(int A, int B);

//Obtenemos el mensaje desde C++
static string GetStringMessage()
{
//Declaramos el objeto que nos devolverá el mensaje
StringBuilder sb = new StringBuilder(STRING_MAX_LENGTH);

//Llamamos a la librería nativa
GetStringMessageNativo(sb, STRING_MAX_LENGTH);

return sb.ToString();
}


static void Main(string[] args)
{
//Obtenemos el sistema operativo sobre el que corre la aplicación
string OS = RuntimeInformation.OSDescription;
int sumando1 = 123, sumando2 = 3245;

Console.WriteLine($"Mensaje escrito en C# sobre {OS}");
Console.WriteLine(GetStringMessage().ToString());
Console.WriteLine(
$"Suma desde código nativo '{sumando1} + {sumando2} = {SumaNativo(sumando1, sumando2)}'");

Console.Read();
}
}
}
En él, vemos que importamos nuestra librería a través de P/Invoke (como se llama igual en todas las plataformas, no necesitamos tener más cosas en cuenta), y después simplemente mostramos por consola un texto con el sistema operativo sobre el que corremos, un mensaje obtenido desde la librería en C++, y la suma de 2 números que también ejecuta el código en C++.

Tenemos que tener en cuenta aquí, que la librería, al no estar referenciada en la solución, no se copiará automáticamente a la carpeta de salida, por lo que es necesario que seamos nosotros quienes la copiemos después de compilar, o en caso contrario tendremos una excepción del tipo System.DllNotFoundException.

Una vez que hemos compilado la solución, y que hemos copiado la librería, vamos a ver qué es lo que nos muestra en los diferentes entornos.

Windows:
Resultado de ejecutar la aplicación en Windows

Linux:
Resultado de ejecutar la aplicación en Linux

MacOS:
Resultado de ejecutar la aplicación en Windows

Como se puede ver, el mismo ejecutable es capaz de consumir las distintas librerías en función de la plataforma, y nos muestra la salida esperada en todas ellas.

Y con esto me despido, para mí ha sido un placer, y espero que os haya gustado el tema, como siempre, os dejo en GitHub el código completo para poder probarlo. Intentaré volver (si José me deja obviamente) con alguna cosa interesante si la situación lo permite.
José M. Aguilar> El placer ha sido nuestro, Jorge. Estaremos atentos a tu blog, y esperamos volver a verte pronto escribiendo por aquí ;) ¡Muchas gracias por publicar en Variable Not Found!

Blog Bitix: Usar expresiones JSONPath para extraer datos de un JSON en Java

$
0
0
Java

Para tratar JSON en Java hay varias alternativas una de ellas es utilizar la API de bajo nivel JSON-P, otra es JSON-B que requiere construir una o varias clases de Java a las que hacer la correspondencia entre el JSON y los objetos Java. Otra alternativa es utilizar expresiones o selectores que seleccionen los datos de JSON de forma similar a como se puede hacer con XPath para el caso de XML o jQuery con los elementos de HTML.

Las expresiones de JSONPath o XPath for JSON se componen de operadores, funciones, operadores de filtrado y predicados con los que dado un JSON y una expresión permite seleccionar, extraer y transformar los datos de forma precisa. La librería JsonPath es una implementación en Java de la especificación JSONPath.

Dado el siguiente texto en JSON estos son algunos ejemplos de expresiones que seleccionan datos utilizando JsonPath.

 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
{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}}}

En estas expresiones por orden se obtienen los autores de los libros de la tienda, los libros de la tienda, los libros cuyo precio es menor que 10, los libros que tienen un atributo isbn, los dos primeros libros y los precios de todos los artículos incluidos los de las bicicletas. En las páginas de JSONPath y de JsonPath hay una documentación más detallada de la sintaxis de las expresiones. JSONPath dispone de un evaluador de expresiones para probar las expresiones de forma rápida.

 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
packageio.github.picodotdev.blogbitix.javajson;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{...// JsonPath
BufferedReaderbr=newBufferedReader(newInputStreamReader(Main.class.getResourceAsStream("/store.json")));StringstoreJson=br.lines().collect(Collectors.joining());br.close();ReadContextreadContext=JsonPath.parse(storeJson);Map<String,String>expressions=newLinkedHashMap<>();expressions.put("authors","$.store.book[*].author");expressions.put("books","$.store.book[*]");expressions.put("cheap-books","$.store.book[?(@.price < 10)]");expressions.put("isbn-books","$.store.book[?(@.isbn)]");expressions.put("first-books","$.store.book[:2]");expressions.put("prices","$..price");expressions.forEach((key,expression)->{Objectvalue=readContext.read(expression);System.out.printf("%s: %s%n",key,value);});}...}
1
2
3
4
5
6
authors: ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]
books: [{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}]
cheap-books: [{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99}]
isbn-books: [{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}]
first-books: [{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99}]
prices: [8.95,12.99,8.99,22.99,19.95]

Estas son las dependencias necesarias para JsonPath y como usa SLF4J varias más para redirigir las trazas a Log4j.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...dependencies{...compile"com.jayway.jsonpath:json-path:2.4.0"runtime'org.apache.logging.log4j:log4j-api:2.11.1'runtime'org.apache.logging.log4j:log4j:2.11.1'runtime'org.apache.logging.log4j:log4j-core:2.11.1'runtime'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6'runtime"org.apache.logging.log4j:log4j-slf4j18-impl:2.11.1"...}

El código equivalente para extraer estos datos usando JSON-P sería más largo, complejo, difícil de mantener y de difícil compresión. Dependiendo de la cantidad de datos a seleccionar se preferirá JSON-B si son muchos o JsonPath si son pocos o hay cierta lógica de filtrado.

La librería JMESPath es una librería equivalente a JsonPath aunque utiliza otra especificación en la que cambia la sintaxis de las expresiones pero no dejan de ser similares, por el hecho de que las expresiones JsonPath siguen el estándar de XPath para XML le da algo de mayor atractivo.

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.

Blog Bitix: Conferencia BilboStack 2019

$
0
0

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

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

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

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

Variable not found: Enlaces interesantes 347

$
0
0
Enlaces 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

Xamarin / Cross-platform

Otros

Publicado en: www.variablenotfound.com.

Variable not found: El hosting in-process de ASP.NET Core 2.2

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

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

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

ASP.NET Core out-of-process

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

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

Hosting in-process

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

¿Qué implica usar el modo in-process?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Publicado en Variable not found.

Fixed Buffer: SourceLink: Depuración de código bajo demanda

$
0
0

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

Blog Bitix: Convertir un JSON a objetos y objetos a JSON con JSON-B, Gson y Jackson en Java

$
0
0
Java

Continuando la serie de pequeños artículos sobre cómo procesar JSON después de ver anteriormente otras dos formas, Generar, procesar y modificar documentos JSON con JSON-P en Java y Usar expresiones JSONPath para extraer datos de un JSON en Java, en este artículo hay una tercera.

En las dos primeras si los datos son muchos o son todos la tarea de recuperar los datos uno a uno requiere una buena cantidad de código. Dado que un JSON no son nada más que valores, arrays y mapas utilizando la estructuras de datos equivalentes de Java se puede hacer una correspondencia entre los datos del JSON a objetos POJO siguiendo ciertas convenciones.

Una vez que los datos han sido cargados en instancias de objetos se recuperan con los correspondientes métodos get que contengan las instancias, además dado que las propiedades de los objetos tiene un tipo se realiza la conversión adecuada para convertir el dato del JSON al tipo de la propiedad del objeto.

Hay tres librerías distintas populares para hacer este binding entre JSON y objetos. JSON-B, Gson y Jackson siendo la primera la más estándar en el lenguaje Java. En los siguientes ejemplos dada una cadena con JSON y la clase raíz a la que hacer la correspondencia de las propiedades se crea una instancia de Comprador y múltiples de Direccion. La correspondencia entre las propiedades del JSON y del objeto se hace en base al nombre.

Se utilizan los métodos toJson() tanto en JSON-B como en Gson y el método writeValueAsString() en Jackson para convertir a JSON y los métodos fromJson() y readValue() para convertir desde JSON a objetos. Estos métodos devuelven una instancia de la clase raíz indicada y acceder a las propiedades se hace con los correspondientes getter.

 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
packageio.github.picodotdev.blogbitix.javajson;...publicclassMain{publicstaticvoidmain(String[]args)throwsException{Compradorcomprador=buildComprador();...// JSON-B
JsonbConfigconfig=newJsonbConfig().withAdapters(newJsonbLocalDateAdapter());Jsonbjsonb=JsonbBuilder.create(config);json=jsonb.toJson(comprador);comprador=jsonb.fromJson(json,Comprador.class);System.out.printf("JSON-B: %s%n",json);System.out.printf("JSON-B (comprador): %s, %s, %d%n",comprador.getNombre(),comprador.getFechaNacimiento(),comprador.getDirecciones().size());// Gson
GsonBuilderbuilder=newGsonBuilder();builder.registerTypeAdapter(LocalDate.class,newGsonLocalDateTypeAdapter());Gsongson=builder.create();;json=gson.toJson(comprador);comprador=gson.fromJson(json,Comprador.class);System.out.printf("Gson: %s%n",json);System.out.printf("Gson (comprador): %s, %s, %d%n",comprador.getNombre(),comprador.getFechaNacimiento(),comprador.getDirecciones().size());// Jackson
ObjectMappermapper=newObjectMapper();SimpleModulemodule=newSimpleModule();module.addSerializer(LocalDate.class,newJacksonLocalDateSerializer());module.addDeserializer(LocalDate.class,newJacksonLocalDateDeserializer());mapper.registerModule(module);json=mapper.writeValueAsString(comprador);comprador=mapper.readValue(json,Comprador.class);System.out.printf("Jackson: %s%n",json);System.out.printf("Jackson (comprador): %s, %s, %d%n",comprador.getNombre(),comprador.getFechaNacimiento(),comprador.getDirecciones().size());...}privatestaticCompradorbuildComprador(){Compradorcomprador=newComprador();comprador.setNombre("Juan");comprador.setFechaNacimiento(LocalDate.now());comprador.getDirecciones().add(buildDireccion());comprador.getDirecciones().add(buildDireccion());returncomprador;}privatestaticDireccionbuildDireccion(){returnnewDireccion("calle","ciudad","codigoPostal","pais");}}
 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
packageio.github.picodotdev.blogbitix.javajson;...publicclassComprador{privateStringnombre;privateLocalDatefechaNacimiento;privateList<Direccion>direcciones;publicComprador(){direcciones=newArrayList<>();}publicStringgetNombre(){returnnombre;}publicvoidsetNombre(Stringnombre){this.nombre=nombre;}publicLocalDategetFechaNacimiento(){returnfechaNacimiento;}publicvoidsetFechaNacimiento(LocalDatefechaNacimiento){this.fechaNacimiento=fechaNacimiento;}publicList<Direccion>getDirecciones(){returndirecciones;}publicvoidsetDirecciones(List<Direccion>direcciones){this.direcciones=direcciones;}}
 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
packageio.github.picodotdev.blogbitix.javajson;publicclassDireccion{privateStringcalle;privateStringciudad;privateStringcodigoPostal;privateStringpais;publicDireccion(){}publicDireccion(Stringcalle,Stringciudad,StringcodigoPostal,Stringpais){this.calle=calle;this.ciudad=ciudad;this.codigoPostal=codigoPostal;this.pais=pais;}publicStringgetCalle(){returncalle;}publicvoidsetCalle(Stringcalle){this.calle=calle;}publicStringgetCiudad(){returnciudad;}publicvoidsetCiudad(Stringciudad){this.ciudad=ciudad;}publicStringgetCodigoPostal(){returncodigoPostal;}publicvoidsetCodigoPostal(StringcodigoPostal){this.codigoPostal=codigoPostal;}publicStringgetPais(){returnpais;}publicvoidsetPais(Stringpais){this.pais=pais;}}
1
2
3
4
5
6
JSON-B: {"direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"},{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}],"fechaNacimiento":"2019-02-01","nombre":"Juan"}
JSON-B (comprador): Juan, 2019-02-01, 2
Gson: {"nombre":"Juan","fechaNacimiento":"2019-02-01","direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"},{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}]}
Gson (comprador): Juan, 2019-02-01, 2
Jackson: {"nombre":"Juan","fechaNacimiento":"2019-02-01","direcciones":[{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"},{"calle":"calle","ciudad":"ciudad","codigoPostal":"codigoPostal","pais":"pais"}]}
Jackson (comprador): Juan, 2019-02-01, 2

Para añadir tipos de datos que no están entre los básicos de JSON como es una fecha cada librería proporciona interfaces o clases abstractas para hacer la conversión desde el dato a un tipo de JSON y desde JSON al tipo del dato. En este caso para un tipo de dato LocalDate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
packageio.github.picodotdev.blogbitix.javajson;...publicclassJsonbLocalDateAdapterimplementsJsonbAdapter<LocalDate,JsonString>{@OverridepublicJsonStringadaptToJson(LocalDateobj)throwsException{returnJson.createValue(obj.format(DateTimeFormatter.ISO_LOCAL_DATE));}@OverridepublicLocalDateadaptFromJson(JsonStringobj)throwsException{returnLocalDate.parse(obj.getString(),DateTimeFormatter.ISO_LOCAL_DATE);}}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
packageio.github.picodotdev.blogbitix.javajson;...publicclassGsonLocalDateTypeAdapterextendsTypeAdapter<LocalDate>{@Overridepublicvoidwrite(JsonWriterout,LocalDatevalue)throwsIOException{out.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE));}@OverridepublicLocalDateread(JsonReaderin)throwsIOException{returnLocalDate.parse(in.nextString(),DateTimeFormatter.ISO_LOCAL_DATE);}}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
packageio.github.picodotdev.blogbitix.javajson;...publicclassJacksonLocalDateSerializerextendsJsonSerializer<LocalDate>{@Overridepublicvoidserialize(LocalDatevalue,JsonGeneratorgen,SerializerProviderserializers)throwsIOException{gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE));}}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
packageio.github.picodotdev.blogbitix.javajson;...publicclassJacksonLocalDateDeserializerextendsJsonDeserializer<LocalDate>{@OverridepublicLocalDatedeserialize(JsonParserp,DeserializationContextctxt)throwsIOException{returnLocalDate.parse(p.getValueAsString(),DateTimeFormatter.ISO_LOCAL_DATE);}}

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.

Referencia:

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

$
0
0

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#

Variable not found: Enlaces interesantes 348

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

Blog Bitix: Log de SQLs y de SQLs lentas en MySQL y PostgreSQL

$
0
0
MySQL
PotgreSQL

La información generalmente y en la mayoría de los casos es de las cosas más importantes de una aplicación. Para guardarla se suelen emplear bases de datos relacionales por sus propiedades de consistencia, transaccionalidad y propiedades ACID aunque más recientemente se ha popularizado otros sistemas no SQL como Redis o Mongo para diferentes casos de uso.

Dos de las bases de datos relacionales más usadas son PostgreSQL y MySQL. El rendimiento de una aplicación en buena parte depende del acceso a la base de datos relacional. Por ello conviene saber cuáles, cuántas consultas se están lanzando al servidor y el tiempo tardan, además suele ser útil monitorizar especialmente las consultas que consideramos lentas al superar cierto umbral de tiempo. Con la información de que consultas, cuantas y las lentas se toman tomar acciones asegurar el buen funcionamiento del sistema, para mejorar el rendimiento de la aplicación optimizando las consultas lentas u optimizando la aplicación para que realice menos consultas al servidor de base de datos si es que hay un problema de 1+N típico en las librerías ORM como Hibernate.

Que una SQL tarde mucho en ejecutarse y consuma muchos recursos del sistema en CPU o memoria potencialmente es un problema grave que posiblemente afecte a funcionalidades importantes de una aplicación, será más acusado si hay un volumen relevante de usuarios usando el sistema simultáneamente. Las consecuencias van desde la caída del servicio hasta tiempos de respuesta elevados.

MySQL

Para activar la generación de logs y de SQLs lentas en MySQL hay que añadir la siguiente configuración a MySQL. Las sentencias lentas que superan cierto tiempo de ejecución son emitidas al archivo mysql-slow.log, según la configuración indicada aquellas que superen 10 segundos. Dado que el ejemplo de consulta es sencilla y la base de datos no es grande la sentencia no aparece en el log de SQLs lentas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version:'3'services:mysql:image:mysql:8volumes:-./configuration/:/etc/mysql/conf.d-./scripts/:/scripts/-mysql-data:/var/lib/mysql-mysql-log:/var/log/mysqlenvironment:MYSQL_ROOT_PASSWORD:rootMYSQL_DATABASE:blogbitixentrypoint:''command:bash-c"chown -R mysql:mysql /var/log/mysql && exec /entrypoint.sh mysqld"volumes:mysql-data:mysql-log:
1
2
3
4
5
6
7
[mysqld]general_log=1general_log_file=/var/log/mysql/mysql.logslow_query_log=1long_query_time=10slow_query_log_file=/var/log/mysql/mysql-slow.log
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
CREATETABLEIFNOTEXISTSPersons(person_idintNOTNULLAUTO_INCREMENTPRIMARYKEY,last_namevarchar(255),first_namevarchar(255));INSERTINTOPersons(last_name,first_name)values('Last name 1','First name 1'),('Last name 2','First name 2'),('Last name 3','First name 3'),('Last name 4','First name 4'),('Last name 5','First name 5'),('Last name 6','First name 6'),('Last name 7','First name 7'),('Last name 8','First name 8'),('Last name 9','First name 9'),('Last name 10','First name 10');
 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
$ docker-compose up
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8d7d4953cb39 mysql:latest "bash -c chown…"10 seconds ago Up 15 seconds 3306/tcp msqsql_msqsql_1
$ docker exec -it 8d7d4953cb39 bash
root@8d7d4953cb39:/# mysql -uroot -proot blogbitix < /scripts/database.sql
root@8d7d4953cb39:/# mysql -uroot -proot
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| blogbitix || information_schema || mysql || performance_schema || sys |
+--------------------+
5 rows in set(0.00 sec)
mysql> use blogbitix;
Database changed
mysql> select * from Persons;
+-----------+--------------+---------------+
| person_id | last_name | first_name |
+-----------+--------------+---------------+
|1| Last name 1| First name 1||2| Last name 2| First name 2||3| Last name 3| First name 3||4| Last name 4| First name 4||5| Last name 5| First name 5||6| Last name 6| First name 6||7| Last name 7| First name 7||8| Last name 8| First name 8||9| Last name 9| First name 9||10| Last name 10| First name 10|
+-----------+--------------+---------------+
10 rows in set(0.00 sec)
root@8d7d4953cb39:/# tail -20 /var/log/mysql/mysql.log
2019-02-04T13:26:48.414716Z 19 Connect root@localhost on using Socket
2019-02-04T13:26:48.414911Z 19 Query select @@version_comment limit 12019-02-04T13:26:53.629645Z 19 Query show databases
2019-02-04T13:26:58.847307Z 19 Query SELECT DATABASE()2019-02-04T13:26:58.847709Z 19 Init DB blogbitix
2019-02-04T13:26:58.851495Z 19 Query show databases
2019-02-04T13:26:58.853238Z 19 Query show tables
2019-02-04T13:26:58.858211Z 19 Field List Persons
2019-02-04T13:27:03.463610Z 19 Query show tables
2019-02-04T13:28:20.522918Z 19 Query select * from Persons
2019-02-04T13:28:35.275031Z 19 Quit 

PostgreSQL

En el caso de PostgeSQL el archivo de log se ubica según el valor de la propiedad log_directory y log_filename. Se activa el log con la propiedad logging_collector. Las sentencias con errores también se incluyen en el mismo archivo. Para obtener los tiempos que tardan las sentencias en ejecutarse hay que establecer un umbral en milisegundos para que la sentencia sea incluida en el log, con el valor 0 se incluyen todas las sentencias en el log en la propiedad log_min_duration_statement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
version:'3'services:postgres:image:postgres:11volumes:-./configuration/postgresql.conf:/etc/postgresql/postgresql.conf-./scripts/:/scripts/-postgres-data:/var/lib/postgresql/data-postgres-log:/var/log/postgresenvironment:POSTGRES_PASSWORD:postgresPOSTGRES_DB:blogbitixcommand:postgres-cconfig_file=/etc/postgresql/postgresql.confvolumes:postgres-data:postgres-log:
 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
...
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
log_destination = 'stderr' # Valid values are combinations of
# stderr, csvlog, syslog, and eventlog,
# depending on platform. csvlog
# requires logging_collector to be on.
# This is used when logging to stderr:
logging_collector = on # Enable capturing of stderr and csvlog
# into log files. Required to be on for
# csvlogs.
# (change requires restart)
# These are only used if logging_collector is on:
log_directory = 'log' # directory where log files are written,
# can be absolute or relative to PGDATA
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
# can include strftime() escapes
...
# - When to Log -
...
log_min_duration_statement = 0 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
# - What to Log -
...
log_statement = 'all' # none, ddl, mod, all
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
CREATETABLEIFNOTEXISTSPersons(person_idSERIALPRIMARYKEY,last_namevarchar(255),first_namevarchar(255));INSERTINTOPersons(last_name,first_name)values('Last name 1','First name 1'),('Last name 2','First name 2'),('Last name 3','First name 3'),('Last name 4','First name 4'),('Last name 5','First name 5'),('Last name 6','First name 6'),('Last name 7','First name 7'),('Last name 8','First name 8'),('Last name 9','First name 9'),('Last name 10','First name 10');
 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
$ docker-compose up
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
448b3ab5684d postgres:latest "docker-entrypoint.s…"6 seconds ago Up 5 seconds 5432/tcp postgresql_postgres_1
$ docker exec -it 448b3ab5684d bash
root@448b3ab5684d:/# psql -U postgres -d blogbitix -f /scripts/database.sql
CREATE TABLE
INSERT 010
root@448b3ab5684d:/# psql -U postgres
psql (11.1 (Debian 11.1-3.pgdg90+1))
Type "help"for help.
postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+------------+------------+-----------------------
blogbitix | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 |=c/postgres +
|||||postgres=CTc/postgres
template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 |=c/postgres +
|||||postgres=CTc/postgres
(4 rows)postgres=# \connect blogbitix
You are now connected to database "blogbitix" as user "postgres".
blogbitix=# select * from Persons;
person_id | last_name | first_name
-----------+--------------+---------------
1| Last name 1| First name 12| Last name 2| First name 23| Last name 3| First name 34| Last name 4| First name 45| Last name 5| First name 56| Last name 6| First name 67| Last name 7| First name 78| Last name 8| First name 89| Last name 9| First name 910| Last name 10| First name 10(10 rows)
root@005d4f42cf44:/# tail /var/lib/postgresql/data/log/postgresql-2019-02-04_151958.log
2019-02-04 15:19:58.980 GMT [25] LOG: database system was shut down at 2019-02-04 15:19:57 GMT
2019-02-04 15:19:58.990 GMT [1] LOG: database system is ready to accept connections
2019-02-04 15:20:13.526 GMT [46] LOG: statement: select * from Persons;2019-02-04 15:20:13.527 GMT [46] LOG: duration: 1.555 ms

Como los archivos de log de sentencias ejecutadas potencialmente serán grandes hay que rotarlos y monitorizar o limitar su tamaño. En PostgreSQL usando las directivas de configuración, log_rotation_age o log_rotation_size, en MySQL posiblemente con el comando logrotate.

Viewing all 2713 articles
Browse latest View live