Conteneur Linux pour l'application .NET Framework (lorsqu'il est difficile de partir pour .Net Core)

Bonjour, Habr.

Je veux partager avec le monde une tâche plutôt atypique, du moins pour moi, et sa solution, qui me semble tout à fait acceptable. Décrit ci-dessous n'est peut-être pas le moyen idéal de sortir de la situation, mais cela fonctionne et fonctionne comme prévu.

Cadre et arrière-plan


Il y avait une tâche à accomplir: vous devez faire des aperçus 3D des modèles BIM de divers équipements, matériaux, objets sur le site. Besoin de quelque chose de léger, simple.

Sur le site Web, les modèles de ces objets sont stockés et disponibles pour téléchargement dans des formats propriétaires de divers systèmes de CAO et sous forme de formats ouverts pour les modèles 3D. Parmi eux, le format IFC . Je vais l'utiliser comme source pour résoudre cette tâche.

L'une des options et ses fonctionnalités


Formellement, on pourrait se limiter à écrire une sorte de convertisseur * .ifc pour quelque chose à afficher sur une page Web. C'est là que j'ai commencé.

Une merveilleuse boîte à outils, la boîte à outils xBIM, a été choisie pour une telle conversion .

Les exemples d'utilisation de cet outil décrivent simplement et clairement comment travailler avec IFC et le * .wexBIM spécialisé pour le format Web.

Tout d'abord, convertissez * .ifc en * .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();
                }
            }
        }
    }
}


Ensuite, le fichier résultant est utilisé dans le "player" xBIM WeXplorer .

Un exemple d'intégration de * .wexBIM dans une page:
<!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>


Eh bien, allons-y. Je prends des pépites de xBIM. J'écris une application console qui reçoit un tas de chemins vers des fichiers * .ifc en entrée, et à côté d'eux, il ajoute un tas de fichiers * .wexBIM. Tout peut être téléchargé sur le site.

Mais en quelque sorte, c'est assez simple ... Je veux que ce programme devienne une sorte de service qui, lors de l'événement de téléchargement * .ifc sur le portail, crée immédiatement le * .wexBIM nécessaire, et il apparaît immédiatement dans le conteneur préparé.

Ok, je forme de nouvelles exigences:

  1. Laissez les tâches de conversion provenir de notre RabbitMQ ;
  2. Je veux voir les tâches elles-mêmes sous la forme d'un message binaire, qui en fait sera prêt pour la désérialisation par la classe décrite dans le fichier protobuf ;
  3. la tâche contiendra un lien pour télécharger le fichier source * .ifc de notre Minio ;
  4. la tâche me dira également quel compartiment dans Minio ajouter le résultat;
  5. laissez l'application elle-même être construite sous .net core 3.1 et travaillez à l'intérieur du conteneur de docker Linux sur notre "batterie de dockers";

Les premières difficultés et conventions


Je ne décrirai pas en détail les 4 premiers points de mise en œuvre. Peut-être plus tard.

Causé l'application à écouter la file d'attente des travaux et envoyer un message avec le résultat à la file d'attente à partir du CorrelationId du message de travail. Vissé les classes de demande / réponse générées à partir de protobuf. A appris à télécharger / télécharger des fichiers en minio .

Tout cela, je le fais dans le projet d'application console. Dans les paramètres du projet:

<TargetFramework>netcoreapp3.1</TargetFramework>

Et sur ma machine avec Windows 10, tout est complètement débogué et fonctionne. Mais lorsque j'essaie d'exécuter l'application en WSL, j'attrape une erreur System.IO.FileLoadException :

Informations d'erreur complètes:
{
  "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
}

Une séance de recherche active sur Google et de lecture réfléchie m'a montré que j'étais extrêmement inattentif:
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.

Et des difficultés similaires ne sont pas pour moi seul. Beaucoup de gens veulent xBIM sous .Net Core .
Pas critique, mais ça change beaucoup ... Tout dépend de l'impossibilité de charger Xbim.Geometry.Engine64.dll normalement . Vous devez avoir vc_redist.x64.exe sur la machine . Quelles sont mes options?
La première chose que j'ai pensé: «Peut-on utiliser un conteneur Windows avec un framework .Net complet?
Livrez Microsoft Visual C ++ Redistributable pour Visual Studio 2015, 2017 et 2019 à ce conteneur, et tout ira bien? » J'ai essayé ceci:

Tester l'image Windows pour docker:
.Net Core :

<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"]

Et bien, ça a marché ... C'est vivant! Mais. Mais qu'en est-il de notre machine Linux hôte avec docker? Cela ne fonctionnera pas pour conduire un conteneur avec une image sur Windows Server Core dessus . Besoin de sortir ...

Compromis et dénouement


Une autre recherche sur le Web m'a amené à un article . Dans ce document, l'auteur nécessite une implémentation similaire:
Pour aggraver les choses:
tous les binaires sont en 32 bits (x86).
Certains nécessitent des composants d'exécution redistribuables C ++ visuels.
Certains nécessitent le runtime .NET.
Certains ont besoin d'un système de fenêtrage, même si nous n'utilisons que l'interface de ligne de commande (CLI).
La publication décrit le potentiel pour exécuter des applications Windows dans Wine dans un conteneur Linux. Curieux, j'ai décidé.

Après quelques tests, bugs et ajouts, le Dockerfile a été reçu:

Image de docker basée sur Ubuntu avec Wine, .Net Framework et vcredist à bord:

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 construction n'est pas rapide, mais se termine avec succès. J'essaye, vérifie. Travaux!

Résultats, conclusions, réflexions


Ça a marché. La sortie est une image Linux pour le conteneur Docker. Il est «gonflé» (~ 5,2 Go), mais il démarre assez rapidement et à l'intérieur, il exécute une application de console Windows sur .Net Framework 4.7, qui écoute RabbitMQ, écrit des journaux dans Graylog , télécharge et télécharge des fichiers vers / dans Minio. Je mettrai à jour l'application par l'API Docker distant.

La solution à la tâche utilitaire dédiée est mise en œuvre. Peut-être, et probablement pas universel. Mais en principe, j'étais satisfait. Peut-être que quelqu'un sera utile aussi.

Merci d'avoir lu. J'écris sur Habr pour la première fois. Rendez-vous dans les commentaires.

All Articles