Primers passos de NET6, C# i Linux

Crides a llibreries

Les crides fins ara es feien des PInvoke a la llibreria de ghostscript de windows. Amb linux no farà falta en si descarregar-afegir la llibreria. També el PInvoke amb linux hi ha unes particularitats 1.

Directoris

Els directoris també canvien. Per a tenir una generació de noms de directoris neta sense directoris a mà, hem de revisar les carpetes especials que fem ús a la API de Path de NET2

Llibreries d’imatges

El fet de no comptar amb System.Drawing dificulta molts dels procesaments de imatge dels que es feia us d’aquesta llibreria de sistema. Cal utilitzar alternatives com Imagesharp o Skiasharp3

Dependències terceres

L’entorn és amb docker/debian per a poder fer el PInvoke a Ghostscript correctament cal instal·lar totes les llibreries següents des el dockerfile:

RUN apt update -y && apt install -y -qq ghostscript && apt install -y -qq libgs9 && apt install -y -qq libgs9-common && apt install -y -qq libgs-dev

  1. https://developers.redhat.com/blog/2016/09/14/pinvoke-in-net-core-rhel ↩︎
  2. https://developers.redhat.com/blog/2018/11/07/dotnet-special-folder-api-linux ↩︎
  3. https://devblogs.microsoft.com/dotnet/net-core-image-processing/ ↩︎

C# i async

Poques ganes:

bool method(){
   var result = classasync.method().Result;
   return result;
}

Venga va, una oportunitat:

async bool method(){
   var result = await classasync.method();
   return result;
}

Però es que no vull fer-lo asíncron:

bool method(){
   var resultTask = Task.Run(async () => await classasync.method());
   resultTask.Wait();
   return resultTask.Result;
}

Va, donem-li una oportunitat a l’asíncron:

async bool method(){
   var resultTask = Task.Run(async () => await classasync.method());
   resultTask.WaitAsync();
   return resultTask.Result;
}

Ajuda: https://ianvink.wordpress.com/2021/12/12/c-running-an-async-await-task-inside-a-non-async-method-code-block/

Clickonce amb Net6

Funcions de System.Deployment.Application

Aquestes ja no estan disponibles amb NET6 [i als docs se fa referència] però afortunadament es poden simular via lectura de les variables d’entorn [informació en aquesta request a github] , dels fitxers de clickonce i dels directoris propis del desplegament.

Fent servir part del codi d’exemple amb de desplegament de una aplicació NET6 WPF com aquest luncher he fet una llibreria tipificant les constants del desplegament que més fem servir així com simular el update silenciós de clickonce que en realitat no és un altre cosa que instal·lar per enrere i apagar la aplicació un cop està instal·lada la nova versió (el restart de NET Framework ara no funcionarà):

https://github.com/Ruekov/ClickOnceNET6

MSBuild

La preferència de les aplicacions amb NET6 és preferible utilitzar la comanda dotnet msbuild el problema més important en aquest cas és que clickonce es fonamenta bàsicament en NET Framework 3.5 així que haurem d’utilitzar MSBuild.

Com passava ja amb MSBuild i clickonce, els perfils o les ordes de publicació es comporten diferent amb Visual Studio que executades des MSBuild. A més a més, tenim el handicap de que les eines de msbuildtasks de loresoft no acaben de funcionar bé.

Per aquesta raó utilitzant els perfils de publicació (Propierties/PublicationProfiles/ClickOnceProfile.pubxml) amb paràmetres postcompilació afegirem la tasca què:

  • Separi els fitxers de desplegament (setup.exe, xxxxx.application, launcher.exe i carpeta ApplicationFiles)
  • Copi un template del index.html per personalitzar-lo (de nou MSBuild no és capaç de fer-lo posant-hi el WebPageFileName a true)
  • Faci un ZIP amb els continguts del deplegament
	<Target Name="ZipPublishOutput" AfterTargets="ZipDeployment">
		<ItemGroup>
			<LauncherFile Include="$(PublishDir)\Launcher.exe"/>
			<ApplicationFile Include="$(PublishDir)\$(MSBuildProjectName).application"/>
			<SetupFile Include="$(PublishDir)\setup.exe"/>
			<ApplicationFiles Include="$(PublishDir)\Application Files\**\*.*"/>
		</ItemGroup>
		<Copy SourceFiles="$(ProjectDir)\index.html" DestinationFiles="$(PublishDir)\..\..\index.html"/>
		<WriteLinesToFile File="$(PublishDir)\..\..\index.html" Lines="$([System.Text.RegularExpressions.Regex]::Replace($([System.IO.File]::ReadAllText('$(PublishDir)\..\..\index.html')), ''{AppVersion}'', ''$(AssemblyVersion)''))" Overwrite="true" Encoding="Unicode" />
		<WriteLinesToFile File="$(PublishDir)\..\..\index.html" Lines="$([System.Text.RegularExpressions.Regex]::Replace($([System.IO.File]::ReadAllText('$(PublishDir)\..\..\index.html')), ''{AppName}'', ''$(MSBuildProjectName)''))" Overwrite="true" Encoding="Unicode" />
		<Copy
			SourceFiles="@(LauncherFile)"
			DestinationFolder="$(PublishDir)\..\..\"
        />
		<Copy
            SourceFiles="@(SetupFile)"
            DestinationFolder="$(PublishDir)\..\..\"
        />
		<Copy
			SourceFiles="@(ApplicationFile)"
			DestinationFolder="$(PublishDir)\..\..\"
        />
		<Copy
            SourceFiles="@(ApplicationFiles)"
            DestinationFolder="$(PublishDir)\..\..\Application Files\%(RecursiveDir)"
        />
		<RemoveDir Directories="$(OutputPath)" />
		<ZipDirectory SourceDirectory="$(PublishDir)\..\..\" DestinationFile="$(PublishDir)\..\..\..\PRO_$(MSBuildProjectName)_$(AssemblyVersion).zip" Overwrite="true" />
	</Target>

Aquest Target haurà de estar especificat al csproj del projecte:

	<Target Name="ZipDeployment">
	</Target>

Al mateix csproj podrem afegir una tasca per mantenir actualitzat als prefils de publicació el número de versió:

<Target Name="updateVersionCLickOnce">
    <PropertyGroup>
        <FilePath>.\Properties\PublishProfiles\ClickOnceProfile.pubxml</FilePathPRO>
    </PropertyGroup>
    <WriteLinesToFile File="$(FilePath)" Lines="$([System.Text.RegularExpressions.Regex]::Replace($([System.IO.File]::ReadAllText('$(FilePath)')), '&gt;\d+\.\d+\.\d+\.\d+&lt;', ''&gt;$(AssemblyVersion)&lt;''))" Overwrite="true" Encoding="Unicode" /> 
</Target>

Al ser una tasca que hauria de ser executada abans de començar la compilació de la publicació haurem de especificar-la al inici del XML del csproj:

<Project Sdk="Microsoft.NET.Sdk" InitialTargets="updateVersionCLickOnce">

Ara ja podem executar el msbuild generant la sortida correcta i esperada:

msbuild /t:Publish,ZipDeployment /p:DeployOnBuild=true /p:PublishProfile=ClickOnceProfile.xml /restore

Recursos

MQTT (I): Posar en marxa un servidor MQTT

Davant de varietat de servidors de MQTT triem per a l’exemple el Mosquitto.

1.- Baixar Mosquitto https://mosquitto.org/download

2.- Anar a la carpeta d’instal·lació i buscar mosquitto.conf. En cas que no existeixi, crear-lo:

3.- Posar la següent configuració:

listener 1883
protocol mqtt
listener 9001
protocol websockets
## Authentication ##
allow_anonymous true
# password_file /mosquitto/conf/mosquitto.conf

4. Provar que funciona correctament arrancant-lo des CMD

.\mosquitto.exe -c .\mosquitto.conf -v

Per a comprovar la connexió i el estat del servidor de MQTT podem fer servir un client bàsic de MQTT fet amb HTML. Com per configuració hem activat els websockets no ens farà falta res més que executar aquesta pàgina des el navegador (les llibreries Javascript és consulten a un CDN):


<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>mqtt</title>
    <script src="https://unpkg.com/mqtt@4.0.1/dist/mqtt.min.js"></script>
    <script>
        // An mqtt variable will be initialized globally
        console.log(mqtt)
    </script>
</head>
<body>

    <script>
        const options = {
            // Clean session
            clean: true,
            connectTimeout: 4000,
        }

        const publishMessage = (event) => {
            event.preventDefault();
            let addr = document.getElementById('addrText').value;
            let topic = document.getElementById('topicText').value;
            let msg = document.getElementById('messageText').value;

            const client = mqtt.connect(addr, options);

            client.publish(topic, msg, { qos: 0, retain: false }, function (error) {
                if (error) {
                    console.log(error);
                } else {
                    console.log('Published');
                }
            })

        }

        const subscribeMessages = (event) => {
            event.preventDefault();

            let addr = document.getElementById('addrTextSub').value;
            let topic = document.getElementById('topicTextSub').value;

            const clientSub = mqtt.connect(addr, options);

            clientSub.on('connect', function () {
                console.log('Connected')
                clientSub.subscribe(topic, function (err) {
                    if (err) {
                        console.log(error);
                    } else {
                        console.log('Subscribed');
                    }
                })
            });

            clientSub.on('message', function (topic, message) {
                // message is Buffer
                console.log();
                var table = document.getElementById("messagesReceived");
                var row = table.insertRow();
                var cell1 = row.insertCell(0);
                var cell2 = row.insertCell(1);
                cell1.innerHTML = new Date().toLocaleString();
                cell2.innerHTML = message.toString();
            });


        }

    </script>

    <form onsubmit="return publishMessage(event)">
        <fieldset>
            <legend>Send message</legend>
            <label for="addrText">Address:</label>
            <input type="text" id="addrText" name="addrText"><br><br>
            <label for="topicText">Topic:</label>
            <input type="text" id="topicText" name="topicText"><br><br>
            <label for="messageText">Message:</label>
            <input type="text" id="messageText" name="messageText"><br><br>
            <input type="submit" value="Submit">
        </fieldset>
    </form>
    <br><br>
    <form onsubmit="return subscribeMessages(event)">
        <fieldset>
            <legend>Subscribe</legend>
            <label for="addrTextSub">Address:</label>
            <input type="text" id="addrTextSub" name="addrTextSub"><br><br>
            <label for="topicTextSub">Topic:</label>
            <input type="text" id="topicTextSub" name="topicTextSub"><br><br>
            <input type="submit" value="Subscribe">
        </fieldset>
    </form>
    <br><br>
    <table id="messagesReceived">
        <tr>
            <th>
                Timestamp
            </th>
            <th>
                Message
            </th>
        </tr>
    </table>
</body>
</html>

Amb el codi anterior i els paràmetres de connexió que al ser per web sockets haurem de especificar protocol (wb) i port a l’adreça tal així: wb://localhost:9001. El resultat de enviar un missatge a la subscripció que triem hauria de ser aquesta:

5.- Feta la prova amb la configuració parem el terminal, farem que sigui un servei i així no necessitem tenir una sessió activa. Per a fer-lo cal executar el mosquito.exe install. En el cas que ja estigui instal·lat és recomanable desinstal·lar-lo executant mosquito.exe uninstall . Per a revisar el estat del servei ho podem fer des Control+Alt+Spr, pestanya Serveis i veure tots els serveis buscant el servei Mosquitto Broker:

Si el servei no funcionés reviseu que a les variables de entorn el MOSQUITTO_DIR és el de instal·lació.

Amb això ja podem iniciar les proves de MQTT amb aquest i altres entorns, recordant que els següents passos amb el servidor haurien de ser dotar-lo de seguretat (autenticació i encriptació del canal).

Google Optimization Tools en una Azure Function

La suite de Optimización de Google, Google Optimization Tools (ortools), ofrece un amplio abanico de herramientas de constraint programming que normalmente son de pago (como Frontline, Groubi ).

Debido a la necesidad de desarrollo de prototipos este solver ofrece una posibilidad de prototipar un desarrollo de manera funcional para saber si los modelos matemáticos son sadisfactibles antes de facturar al cliente un proyecto complejo.

Para desgracia de nuestro entorno de trabajo y como ya es constumbre en Google, los wrappers de C# estan en funciones obsoletas. Para nuestra fortuna, la libreria de Python está al día aunque a día de hoy (09-2018) Python en Azure Functions a dia de hoy está en experimental.

1.- Instalar Python x64 a la Azure Function

La versión que por defecto funciona en Azure Function es la 2.6, la cual el package de ortools NO soporta. También por defecto se ejecuta en x86 así que procederemos a activar el x64 y posteriormente instalar la versión más reciente que nos permiten desde Azure, la 3.6.4.

Primero activamos el modo x64 al servidor de funciones:

Platform features > Application settings y click a 64-bit a Platform

A partir de ahora tendremos que realizar acciones de consola. Algunas se pueden hacer visualmente, pero a través de consola se explican aquí o aquí. De manera visual hay que acceder al kudu a través de esta dirección:

https://[NombreDelServerDeFunciones].scm.azurewebsites.net

Accedemos entonces a “Site extensions” y añadimos Python 3.6.4 x64

Posteriormente volvemos a Platform features > Application settings para añadir estas lineas:

Handler mappings

EXTENSION
SCRIPT PROCESSOR
ARGUMENTS

Application settings

APP SETTING NAME
VALUE
0

Una vez finalizados estos pasos, podemos proceder a reiniciar el servidor de App Functions y proceder a instalar la libreria ortools con pip.

Volvemos a Kudu y tecleamos la instalación de ortools a través a pip:

python -m pip install ortools