Contenedor de Linux para la aplicación .NET Framework (cuando es difícil salir para .Net Core)

Hola Habr

Quiero compartir con el mundo una tarea bastante atípica, al menos para mí, y su solución, que me parece bastante aceptable. Descrito a continuación puede no ser la salida ideal de la situación, pero funciona y funciona según lo previsto.

Entorno y antecedentes


Había una tarea para el trabajo: tenía que hacer vistas previas en 3D de los modelos BIM de varios equipos, materiales y objetos en el sitio. Necesita algo ligero, sin complicaciones.

En el sitio web, los modelos de estos objetos se almacenan y se pueden descargar en formatos propietarios de varios sistemas CAD y en forma de formatos abiertos para modelos 3D. Entre ellos está el formato IFC . Lo usaré como fuente para resolver esta tarea.

Una de las opciones y sus características.


Formalmente, uno podría restringirse a escribir algún tipo de convertidor * .ifc para que algo se muestre en una página web. Aquí es donde empecé.

Se eligió un maravilloso kit de herramientas, el xBIM Toolkit, para tal conversión .

Los ejemplos de uso de esta herramienta describen de manera simple y clara cómo trabajar con IFC y * .wexBIM especializado para el formato web.

Primero, convierta * .ifc a * .wexBIM:
using System.IO;
using Xbim.Ifc;
using Xbim.ModelGeometry.Scene;

namespace CreateWexBIM
{
    class Program
    {
        public static void Main()
        {
            const string fileName = "SampleHouse.ifc";
            using (var model = IfcStore.Open(fileName))
            {
                var context = new Xbim3DModelContext(model);
                context.CreateContext();

                var wexBimFilename = Path.ChangeExtension(fileName, "wexBIM");
                using (var wexBiMfile = File.Create(wexBimFilename))
                {
                    using (var wexBimBinaryWriter = new BinaryWriter(wexBiMfile))
                    {
                        model.SaveAsWexBim(wexBimBinaryWriter);
                        wexBimBinaryWriter.Close();
                    }
                    wexBiMfile.Close();
                }
            }
        }
    }
}


A continuación, el archivo resultante se usa en el "reproductor" xBIM WeXplorer .

Un ejemplo de incrustación * .wexBIM en una página:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hello building!</title>
    <script src="js/xbim-viewer.debug.bundle.js"></script>
</head>
<body>
    <div id="content">
        <canvas id="viewer" width="500" height="300"></canvas>
        <script type="text/javascript">
            var viewer = new xViewer('viewer');
            viewer.load('data/SampleHouse.wexbim');
            viewer.start();
        </script>
    </div>    
</body>
</html>


Bueno, vamos. Tomo nugets de xBIM. Estoy escribiendo una aplicación de consola que recibe un montón de rutas a los archivos * .ifc como entrada, y junto a ellas agrega un montón de archivos * .wexBIM. Todo se puede cargar en el sitio.

Pero de alguna manera es bastante simple ... Quiero que este programa se convierta en un tipo de servicio, que, en el evento de carga * .ifc al portal, crea inmediatamente el * .wexBIM necesario, y aparece inmediatamente en el contenedor preparado.

Ok, formulo nuevos requisitos:

  1. Deje que las tareas de conversión provengan de nuestro RabbitMQ ;
  2. Quiero ver las tareas en forma de mensaje binario, que de hecho estará listo para la deserialización por la clase descrita en el archivo protobuf ;
  3. la tarea contendrá un enlace para descargar el archivo fuente * .ifc de nuestro Minio ;
  4. la tarea también me dirá qué cubo en Minio agregar el resultado;
  5. deje que la aplicación misma se construya bajo .net core 3.1 y trabaje dentro del contenedor docker de Linux en nuestra "granja de docker";

Las primeras dificultades y convenciones


No describiré en detalle los primeros 4 puntos de implementación. Quizas mas tarde.

Causó que la aplicación escuchara la cola del trabajo y enviara un mensaje con el resultado a la cola desde el CorrelationId del mensaje del trabajo. Atornilló las clases de solicitud / respuesta generadas desde protobuf. Enseñó a descargar / cargar archivos en minio .

Todo esto lo hago en el proyecto de aplicación de consola. En la configuración del proyecto:

<TargetFramework>netcoreapp3.1</TargetFramework>

Y en mi máquina con Windows 10 todo está completamente depurado y funciona. Pero cuando intento ejecutar la aplicación en WSL, me da un error de System.IO.FileLoadException :

Información completa del error:
{
  "Type": "System.IO.FileLoadException",
  "Message": "Failed to load Xbim.Geometry.Engine64.dll",
  "TargetSite": "Void .ctor(Microsoft.Extensions.Logging.ILogger`1[Xbim.Geometry.Engine.Interop.XbimGeometryEngine])",
  "StackTrace": " at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.get_Engine()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.CreateContext(ReportProgressDelegate progDelegate, Boolean adjustWcs)\r\n at My.Converter.ConvertIfc.CreateWebIfc(String ifcFileFullPath, String wexBIMFolder)",
  "Data": {},
  "InnerException": {
    "Type": "System.IO.FileNotFoundException",
    "Message": "Could not load file or assembly 'Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.",
    "FileName": "Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null",
    "FusionLog": "",
    "TargetSite": "System.Reflection.RuntimeAssembly nLoad(System.Reflection.AssemblyName, System.String, System.Reflection.RuntimeAssembly, System.Threading.StackCrawlMark ByRef, Boolean, System.Runtime.Loader.AssemblyLoadContext)",
    "StackTrace": " at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, RuntimeAssembly assemblyContext, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, StackCrawlMark& stackMark, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.Assembly.Load(String assemblyString)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)",
    "Data": {},
    "Source": "System.Private.CoreLib",
    "HResult": -2147024894
  },
  "Source": "Xbim.Geometry.Engine.Interop",
  "HResult": -2146232799
}

Una sesión de búsqueda activa en Google y lectura reflexiva me mostró que era extremadamente desatento:
Recently at work, we were evaluating a few options to render building models in the browser. Building Information Modeling (BIM) in interoperability scenarios is done via Industry Foundation Classes, mostly in the STEP Physical File format. The schema is quite huge and complex with all the things you have to consider, so we were glad to find the xBim open source project on GitHub. They've got both projects to visualize building models in the browser with WebGL as well as conversion tools to create the binary-formatted geometry mesh. To achieve that, native C++ libraries are dynamically loaded (so no .Net Core compatibility) which must be present in the bin folder. The C++ libraries are expected either in the same folder as the application binaries or in a x64 (or x86, respectively) sub folder (See here for more details). In regular projects, the xBim.Geometry NuGet package adds a build task to copy the dlls into the build output folder, but this doesn't work with the new tooling. You can, however, get it to work in Visual Studio 2015 by taking care of supplying the interop dlls yourself.

Y dificultades similares no son solo para mí. Mucha gente quiere xBIM bajo .Net Core .
No es crítico, pero cambia mucho ... Todo depende de la incapacidad de cargar Xbim.Geometry.Engine64.dll normalmente . Necesita tener vc_redist.x64.exe en la máquina . ¿Cuáles son mis opciones?
Lo primero que pensé: “¿Se puede usar un contenedor de Windows con un .Net Framework completo?
Entregue Microsoft Visual C ++ Redistribuible para Visual Studio 2015, 2017 y 2019 a este contenedor, ¿y todo estará bien? Intenté esto:

Prueba de imagen de Windows para Docker:
Cambió .Net Core a:

<TargetFramework>net47</TargetFramework>

Dockerfile:

FROM microsoft/dotnet-framework:4.7
WORKDIR /bimlibconverter
COPY lib/VC_redist.x64.exe /VC_redist.x64.exe
RUN C:\VC_redist.x64.exe /quiet /install
COPY bin/Release .
ENTRYPOINT ["MyConverter.exe"]

Bueno, funcionó ... ¡Está vivo! Pero. Pero, ¿qué pasa con nuestra máquina host Linux con docker? No funcionará manejar un contenedor con una imagen en Windows Server Core . Necesito salir ...

Compromiso y desenlace


Otra búsqueda en la web me llevó a un artículo . En él, el autor requiere una implementación similar:
Para empeorar las cosas:
todos los binarios son de 32 bits (x86).
Algunos requieren componentes visuales redistribuibles de C ++ en tiempo de ejecución.
Algunos requieren el tiempo de ejecución .NET.
Algunos necesitan un sistema de ventanas, aunque solo usamos la interfaz de línea de comandos (CLI).
La publicación describe el potencial para ejecutar aplicaciones de Windows en wine en un contenedor de Linux. Curioso, decidí.

Después de algunas pruebas, errores y adiciones, se recibió el Dockerfile:

Imagen de docker basada en Ubuntu con Wine, .Net Framework y vcredist a bordo:

FROM ubuntu:latest
#  x86
RUN dpkg --add-architecture i386 \
    && apt-get update \
    #   
    && apt-get install -qfy --install-recommends \
        software-properties-common \
        gnupg2 \
        wget \
        xvfb \
        cabextract \
    #  Wine
    && wget -nv https://dl.winehq.org/wine-builds/winehq.key \
    && apt-key add winehq.key \
    && apt-add-repository 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' \
    #     Wine
    && add-apt-repository ppa:cybermax-dexter/sdl2-backport \
    #  Wine
    && apt-get install -qfy --install-recommends \
        winehq-staging \
        winbind \
    # 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo
#    Wine
ENV WINEDEBUG=fixme-all
ENV WINEPREFIX=/root/.net
ENV WINEARCH=win64
#  Wine
RUN winecfg \
    # winetricks,   .Net Framework  
    && wget https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks \
    -O /usr/local/bin/winetricks \
    && chmod +x /usr/local/bin/winetricks \
# 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo \
    # Wine   
    && wineboot -u && winetricks -q dotnet472 && xvfb-run winetricks -q vcrun2015

WORKDIR /root/.net/drive_c/myconverter/

#  
COPY /bin/Release/ /root/.net/drive_c/myconverter/

ENTRYPOINT ["wine", "MyConverter.exe"]

UPD: . rueler

La construcción no es rápida, pero finaliza con éxito. Lo intento, verifica. ¡Trabajos!

Resultados, conclusiones, pensamientos.


Funcionó. El resultado es una imagen de Linux para el contenedor docker. Está "hinchado" (~ 5.2GB), pero comienza bastante rápido y en su interior ejecuta una aplicación de consola de Windows en .Net Framework 4.7, que escucha RabbitMQ, escribe registros en Graylog , descarga y carga archivos en / en Minio. Actualizaré la aplicación mediante la API de acoplador remoto.

Se implementa la solución a la tarea utilitaria dedicada. Quizás, y muy probablemente, no universal. Pero en principio estaba satisfecho. Tal vez alguien sea útil también.

Gracias por leer. Escribo en Habr por primera vez. Nos vemos en los comentarios.

All Articles